diff --git a/contrib/pg_upgrade_support/pg_upgrade_support.c b/contrib/pg_upgrade_support/pg_upgrade_support.c index 8b0e474dce51a5d51e89e3b15e3fb36ab41504d3..02d1512719ec2a7f11e938145b144df39be03f0f 100644 --- a/contrib/pg_upgrade_support/pg_upgrade_support.c +++ b/contrib/pg_upgrade_support/pg_upgrade_support.c @@ -150,16 +150,11 @@ create_empty_extension(PG_FUNCTION_ARGS) text *extName = PG_GETARG_TEXT_PP(0); text *schemaName = PG_GETARG_TEXT_PP(1); bool relocatable = PG_GETARG_BOOL(2); - char *extVersion; + text *extVersion = PG_GETARG_TEXT_PP(3); Datum extConfig; Datum extCondition; List *requiredExtensions; - if (PG_ARGISNULL(3)) - extVersion = NULL; - else - extVersion = text_to_cstring(PG_GETARG_TEXT_PP(3)); - if (PG_ARGISNULL(4)) extConfig = PointerGetDatum(NULL); else @@ -195,7 +190,7 @@ create_empty_extension(PG_FUNCTION_ARGS) GetUserId(), get_namespace_oid(text_to_cstring(schemaName), false), relocatable, - extVersion, + text_to_cstring(extVersion), extConfig, extCondition, requiredExtensions); diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 24aa22cbced804c05df94a65aada91c6e69f4bf2..a373829d39d740adc6bca594e1ba301e80f3be16 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -2927,7 +2927,7 @@ <entry><structfield>extversion</structfield></entry> <entry><type>text</type></entry> <entry></entry> - <entry>Version string for the extension, or <literal>NULL</> if none</entry> + <entry>Version name for the extension</entry> </row> <row> diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 31ea0487f1e52202d704da7a5ef20b9a39c39a7c..93bcba9a10cfb299074747eca8052e9451f1f05e 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -304,7 +304,7 @@ </para> <para> - The advantage of using an extension, rather than just running the + The main advantage of using an extension, rather than just running the <acronym>SQL</> script to load a bunch of <quote>loose</> objects into your database, is that <productname>PostgreSQL</> will then understand that the objects of the extension go together. You can @@ -331,6 +331,17 @@ data; see below.) </para> + <para> + The extension mechanism also has provisions for packaging modification + scripts that adjust the definitions of the SQL objects contained in an + extension. For example, if version 1.1 of an extension adds one function + and changes the body of another function compared to 1.0, the extension + author can provide an <firstterm>update script</> that makes just those + two changes. The <command>ALTER EXTENSION UPDATE</> command can then + be used to apply these changes and track which version of the extension + is actually installed in a given database. + </para> + <para> The kinds of SQL objects that can be members of an extension are shown in the description of <xref linkend="sql-alterextension">. Notably, objects @@ -355,10 +366,13 @@ file for each extension, which must be named the same as the extension with a suffix of <literal>.control</>, and must be placed in the installation's <literal>SHAREDIR/contrib</literal> directory. There - must also be a <acronym>SQL</> script file, which typically is - named after the extension with a suffix of <literal>.sql</>, and is also - placed in the <literal>SHAREDIR/contrib</literal> directory; but these - defaults can be overridden by the control file. + must also be at least one <acronym>SQL</> script file, which follows the + naming pattern + <literal><replaceable>extension</>-<replaceable>version</>.sql</literal> + (for example, <literal>foo-1.0.sql</> for version <literal>1.0</> of + extension <literal>foo</>). By default, the script file(s) are also + placed in the <literal>SHAREDIR/contrib</literal> directory; but the + control file can specify a different directory for the script file(s). </para> <para> @@ -376,23 +390,25 @@ <variablelist> <varlistentry> - <term><varname>script</varname> (<type>string</type>)</term> + <term><varname>directory</varname> (<type>string</type>)</term> <listitem> <para> - The filename of the extension's <acronym>SQL</> script. - Defaults to the same name as the control file, but with the - <literal>.sql</literal> extension. Unless an absolute path is - given, the name is relative to the <literal>SHAREDIR/contrib</literal> - directory. + The directory containing the extension's <acronym>SQL</> script + file(s). Unless an absolute path is given, the name is relative to + the <literal>SHAREDIR/contrib</literal> directory. </para> </listitem> </varlistentry> <varlistentry> - <term><varname>version</varname> (<type>string</type>)</term> + <term><varname>default_version</varname> (<type>string</type>)</term> <listitem> <para> - The version of the extension. Any string can be given. + The default version of the extension (the one that will be installed + if no version is specified in <command>CREATE EXTENSION</>). Although + this can be omitted, that will result in <command>CREATE EXTENSION</> + failing if no <literal>VERSION</> option appears, so you generally + don't want to do that. </para> </listitem> </varlistentry> @@ -403,7 +419,7 @@ <para> A comment (any string) about the extension. Alternatively, the comment can be set by means of the <xref linkend="sql-comment"> - command. + command in the script file. </para> </listitem> </varlistentry> @@ -423,10 +439,9 @@ <term><varname>encoding</varname> (<type>string</type>)</term> <listitem> <para> - The character set encoding used by the script file. This should - be specified if the script file contains any non-ASCII characters. - Otherwise the script will be assumed to be in the encoding of the - database it is loaded into. + The character set encoding used by the script file(s). This should + be specified if the script files contain any non-ASCII characters. + Otherwise the files will be assumed to be in the database encoding. </para> </listitem> </varlistentry> @@ -457,22 +472,37 @@ </variablelist> <para> - An extension's <acronym>SQL</> script file can contain any SQL commands, + In addition to the primary control file + <literal><replaceable>extension</>.control</literal>, + an extension can have secondary control files named in the style + <literal><replaceable>extension</>-<replaceable>version</>.control</literal>. + If supplied, these must be located in the script file directory. + Secondary control files follow the same format as the primary control + file. Any parameters set in a secondary control file override the + primary control file when installing or updating to that version of + the extension. However, the parameters <varname>directory</>, + <varname>default_version</>, and <varname>encoding</> cannot be set in + a secondary control file; in particular, the same encoding must be used + in all script files associated with the extension. + </para> + + <para> + An extension's <acronym>SQL</> script files can contain any SQL commands, except for transaction control commands (<command>BEGIN</>, <command>COMMIT</>, etc) and commands that cannot be executed inside a transaction block (such as <command>VACUUM</>). This is because the - script file is implicitly executed within a transaction block. + script files are implicitly executed within a transaction block. </para> <para> - While the script file can contain any characters allowed by the specified - encoding, the control file should contain only plain ASCII, because there - is no way for <productname>PostgreSQL</> to know what encoding the + While the script files can contain any characters allowed by the specified + encoding, control files should contain only plain ASCII, because there + is no way for <productname>PostgreSQL</> to know what encoding a control file is in. In practice this is only an issue if you want to use non-ASCII characters in the extension's comment. Recommended - practice in that case is to not use the <varname>comment</> parameter - in the control file, but instead use <command>COMMENT ON EXTENSION</> - within the script file to set the comment. + practice in that case is to not use the control file <varname>comment</> + parameter, but instead use <command>COMMENT ON EXTENSION</> + within a script file to set the comment. </para> </sect2> @@ -629,6 +659,91 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr </para> </sect2> + <sect2> + <title>Extension Updates</title> + + <para> + One advantage of the extension mechanism is that it provides convenient + ways to manage updates to the SQL commands that define an extension's + objects. This is done by associating a version name or number with + each released version of the extension's installation script. + In addition, if you want users to be able to update their databases + dynamically from one version to the next, you should provide + <firstterm>update scripts</> that make the necessary changes to go from + one version to the next. Update scripts have names following the pattern + <literal><replaceable>extension</>-<replaceable>oldversion</>-<replaceable>newversion</>.sql</literal> + (for example, <literal>foo-1.0-1.1.sql</> contains the commands to modify + version <literal>1.0</> of extension <literal>foo</> into version + <literal>1.1</>). + </para> + + <para> + Given that a suitable update script is available, the command + <command>ALTER EXTENSION ... UPDATE</> will update an installed extension + to the specified new version. The update script is run in the same + environment that <command>CREATE EXTENSION</> provides for installation + scripts: in particular, <varname>search_path</> is set up in the same + way, and any new objects created by the script are automatically added + to the extension. + </para> + + <para> + The update mechanism can be used to solve an important special case: + converting a <quote>loose</> collection of objects into an extension. + Before the extension mechanism was added to + <productname>PostgreSQL</productname> (in 9.1), many people wrote + extension modules that simply created assorted unpackaged objects. + Given an existing database containing such objects, how can we convert + the objects into a properly packaged extension? Dropping them and then + doing a plain <command>CREATE EXTENSION</> is one way, but it's not + desirable if the objects have dependencies (for example, if there are + table columns of a data type created by the extension). The way to fix + this situation is to create an empty extension, then use <command>ALTER + EXTENSION ADD</> to attach each pre-existing object to the extension, + then finally create any new objects that are in the current extension + version but were not in the unpackaged release. <command>CREATE + EXTENSION</> supports this case with its <literal>FROM</> <replaceable + class="parameter">old_version</> option, which causes it to not run the + normal installation script for the target version, but instead the update + script named + <literal><replaceable>extension</>-<replaceable>old_version</>-<replaceable>target_version</>.sql</literal>. + The choice of the dummy version name to use as <replaceable + class="parameter">old_version</> is up to the extension author, though + <literal>unpackaged</> is a common convention. If you have multiple + prior versions you need to be able to update into extension style, use + multiple dummy version names to identify them. + </para> + + <para> + <command>ALTER EXTENSION</> is able to execute sequences of update + script files to achieve a requested update. For example, if only + <literal>foo-1.0-1.1.sql</> and <literal>foo-1.1-2.0.sql</> are + available, <command>ALTER EXTENSION</> will apply them in sequence if an + update to version <literal>2.0</> is requested when <literal>1.0</> is + currently installed. + </para> + + <para> + <productname>PostgreSQL</> doesn't assume anything about the properties + of version names: for example, it does not know whether <literal>1.1</> + follows <literal>1.0</>. It just matches up the available version names + and follows the path that requires applying the fewest update scripts. + </para> + + <para> + Sometimes it is useful to provide <quote>downgrade</> scripts, for + example <literal>foo-1.1-1.0.sql</> to allow reverting the changes + associated with version <literal>1.1</>. If you do that, be careful + of the possibility that a downgrade script might unexpectedly + get applied because it yields a shorter path. The risky case is where + there is a <quote>fast path</> update script that jumps ahead several + versions as well as a downgrade script to the fast path's start point. + It might take fewer steps to apply the downgrade and then the fast + path than to move ahead one version at a time. If the downgrade script + drops any irreplaceable objects, this will yield undesirable results. + </para> + </sect2> + <sect2> <title>Extension Example</title> @@ -640,7 +755,7 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr </para> <para> - The script file <filename>pair.sql</> looks like this: + The script file <filename>pair-1.0.sql</> looks like this: <programlisting><![CDATA[ CREATE TYPE pair AS ( k text, v text ); @@ -671,7 +786,7 @@ CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, PROCEDURE = pair); <programlisting> # pair extension comment = 'A key/value pair data type' -version = '0.1.2' +default_version = '1.0' relocatable = true </programlisting> </para> @@ -682,7 +797,7 @@ relocatable = true <programlisting> EXTENSION = pair -DATA = pair.sql +DATA = pair-1.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) @@ -739,7 +854,7 @@ include $(PGXS) <programlisting> MODULES = isbn_issn EXTENSION = isbn_issn -DATA_built = isbn_issn.sql +DATA_built = isbn_issn-1.0.sql DOCS = README.isbn_issn PG_CONFIG = pg_config diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml index e9eb1aafbb6f3552a096f3c872bcf0d3f1ad7f3f..a6c0062fe240905c4f5d010aad656c3a5725f49a 100644 --- a/doc/src/sgml/ref/alter_extension.sgml +++ b/doc/src/sgml/ref/alter_extension.sgml @@ -23,6 +23,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> +ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> UPDATE [ TO <replaceable class="PARAMETER">new_version</replaceable> ] ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> SET SCHEMA <replaceable class="PARAMETER">new_schema</replaceable> ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> ADD <replaceable class="PARAMETER">member_object</replaceable> ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP <replaceable class="PARAMETER">member_object</replaceable> @@ -61,6 +62,17 @@ ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP extension. There are several subforms: <variablelist> + <varlistentry> + <term><literal>UPDATE</literal></term> + <listitem> + <para> + This form updates the extension to a newer version. The extension + must supply a suitable update script (or series of scripts) that can + modify the currently-installed version into the requested version. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>SET SCHEMA</literal></term> <listitem> @@ -77,7 +89,7 @@ ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP <listitem> <para> This form adds an existing object to the extension. This is mainly - useful in extension upgrade scripts. The object will subsequently + useful in extension update scripts. The object will subsequently be treated as a member of the extension; notably, it can only be dropped by dropping the extension. </para> @@ -89,7 +101,7 @@ ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP <listitem> <para> This form removes a member object from the extension. This is mainly - useful in extension upgrade scripts. The object is not dropped, only + useful in extension update scripts. The object is not dropped, only disassociated from the extension. </para> </listitem> @@ -119,6 +131,18 @@ ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP </listitem> </varlistentry> + <varlistentry> + <term><replaceable class="PARAMETER">new_version</replaceable></term> + <listitem> + <para> + The desired new version of the extension. This can be written as + either an identifier or a string literal. If not specified, + <command>ALTER EXTENSION UPDATE</> attempts to update to whatever is + shown as the default version in the extension's control file. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><replaceable class="PARAMETER">new_schema</replaceable></term> <listitem> @@ -231,7 +255,14 @@ ALTER EXTENSION <replaceable class="PARAMETER">extension_name</replaceable> DROP <title>Examples</title> <para> - To change the schema of the extension <literal>hstore</literal> + To update the <literal>hstore</literal> extension to version 2.0: +<programlisting> +ALTER EXTENSION hstore UPDATE TO '2.0'; +</programlisting> + </para> + + <para> + To change the schema of the <literal>hstore</literal> extension to <literal>utils</literal>: <programlisting> ALTER EXTENSION hstore SET SCHEMA utils; diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml index 961cab3839e0ee2d9a4292e4f95cf34a03469e27..9e0e3c440b6d7b11461a095372c53cdbb327f0ef 100644 --- a/doc/src/sgml/ref/create_extension.sgml +++ b/doc/src/sgml/ref/create_extension.sgml @@ -22,7 +22,9 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE EXTENSION <replaceable class="parameter">extension_name</replaceable> - [ WITH ] [ SCHEMA [=] <replaceable class="parameter">schema</replaceable> ] + [ WITH ] [ SCHEMA <replaceable class="parameter">schema</replaceable> ] + [ VERSION <replaceable class="parameter">version</replaceable> ] + [ FROM <replaceable class="parameter">old_version</replaceable> ] </synopsis> </refsynopsisdiv> @@ -82,6 +84,44 @@ CREATE EXTENSION <replaceable class="parameter">extension_name</replaceable> </para> </listitem> </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">version</replaceable></term> + <listitem> + <para> + The version of the extension to install. This can be written as + either an identifier or a string literal. The default version is + whatever is specified in the extension's control file. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">old_version</replaceable></term> + <listitem> + <para> + <literal>FROM</> <replaceable class="parameter">old_version</> + must be specified when, and only when, you are attempting to install + an extension that replaces an <quote>old style</> module that is just + a collection of objects not packaged into an extension. This option + causes <command>CREATE EXTENSION</> to run an alternative installation + script that absorbs the existing objects into the extension, instead + of creating new objects. Be careful that <literal>SCHEMA</> specifies + the schema containing these pre-existing objects. + </para> + + <para> + The value to use for <replaceable + class="parameter">old_version</replaceable> is determined by the + extension's author, and might vary if there is more than one version + of the old-style module that can be upgraded into an extension. + For the standard additional modules supplied with pre-9.1 + <productname>PostgreSQL</productname>, use <literal>unpackaged</> + for <replaceable class="parameter">old_version</replaceable> when + updating a module to extension style. + </para> + </listitem> + </varlistentry> </variablelist> </refsect1> @@ -95,6 +135,16 @@ CREATE EXTENSION <replaceable class="parameter">extension_name</replaceable> CREATE EXTENSION hstore; </programlisting> </para> + + <para> + Update a pre-9.1 installation of <literal>hstore</> into + extension style: +<programlisting> +CREATE EXTENSION hstore SCHEMA public FROM unpackaged; +</programlisting> + Be careful to specify the schema in which you installed the existing + <literal>hstore</> objects. + </para> </refsect1> <refsect1> diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index bc121808bec094299bdd17faf752f6db018bd6b1..5d8b36b0966962048221380e5b6f5bfd788c0e1c 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -53,17 +53,21 @@ #include "utils/tqual.h" +/* Globally visible state variables */ bool creating_extension = false; Oid CurrentExtensionObject = InvalidOid; +/* Character that separates extension & version names in a script filename */ +#define EXT_VERSION_SEP '-' + /* * Internal data structure to hold the results of parsing a control file */ typedef struct ExtensionControlFile { char *name; /* name of the extension */ - char *script; /* filename of the installation script */ - char *version; /* version ID, if any */ + char *directory; /* directory for script files */ + char *default_version; /* default install target version, if any */ char *comment; /* comment, if any */ char *schema; /* target schema (allowed if !relocatable) */ bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */ @@ -71,6 +75,19 @@ typedef struct ExtensionControlFile List *requires; /* names of prerequisite extensions */ } ExtensionControlFile; +/* + * Internal data structure for update path information + */ +typedef struct ExtensionVersionInfo +{ + char *name; /* name of the starting version */ + List *reachable; /* List of ExtensionVersionInfo's */ + /* working state for Dijkstra's algorithm: */ + bool distance_known; /* is distance from start known yet? */ + int distance; /* current worst-case distance estimate */ + struct ExtensionVersionInfo *previous; /* current best predecessor */ +} ExtensionVersionInfo; + /* * get_extension_oid - given an extension name, look up the OID @@ -196,6 +213,44 @@ get_extension_schema(Oid ext_oid) return result; } +/* + * Utility functions to check validity of extension and version names + */ +static void +check_valid_extension_name(const char *extensionname) +{ + /* + * No directory separators (this is sufficient to prevent ".." style + * attacks). + */ + if (first_dir_separator(extensionname) != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension name: \"%s\"", extensionname), + errdetail("Extension names must not contain directory separator characters."))); +} + +static void +check_valid_version_name(const char *versionname) +{ + /* No separators --- would risk confusion of install vs update scripts */ + if (strchr(versionname, EXT_VERSION_SEP)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension version name: \"%s\"", versionname), + errdetail("Version names must not contain the character \"%c\".", + EXT_VERSION_SEP))); + /* + * No directory separators (this is sufficient to prevent ".." style + * attacks). + */ + if (first_dir_separator(versionname) != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension version name: \"%s\"", versionname), + errdetail("Version names must not contain directory separator characters."))); +} + /* * Utility functions to handle extension-related path names */ @@ -207,6 +262,14 @@ is_extension_control_filename(const char *filename) return (extension != NULL) && (strcmp(extension, ".control") == 0); } +static bool +is_extension_script_filename(const char *filename) +{ + const char *extension = strrchr(filename, '.'); + + return (extension != NULL) && (strcmp(extension, ".sql") == 0); +} + static char * get_extension_control_directory(void) { @@ -228,83 +291,150 @@ get_extension_control_filename(const char *extname) get_share_path(my_exec_path, sharepath); result = (char *) palloc(MAXPGPATH); - snprintf(result, MAXPGPATH, "%s/contrib/%s.control", sharepath, extname); + snprintf(result, MAXPGPATH, "%s/contrib/%s.control", + sharepath, extname); return result; } -/* - * Given a relative pathname such as "name.sql", return the full path to - * the script file. If given an absolute name, just return it. - */ static char * -get_extension_absolute_path(const char *filename) +get_extension_script_directory(ExtensionControlFile *control) { char sharepath[MAXPGPATH]; char *result; - if (is_absolute_path(filename)) - return pstrdup(filename); + /* + * The directory parameter can be omitted, absolute, or relative to the + * control-file directory. + */ + if (!control->directory) + return get_extension_control_directory(); + + if (is_absolute_path(control->directory)) + return pstrdup(control->directory); get_share_path(my_exec_path, sharepath); result = (char *) palloc(MAXPGPATH); - snprintf(result, MAXPGPATH, "%s/contrib/%s", sharepath, filename); + snprintf(result, MAXPGPATH, "%s/contrib/%s", + sharepath, control->directory); + + return result; +} + +static char * +get_extension_aux_control_filename(ExtensionControlFile *control, + const char *version) +{ + char *result; + char *scriptdir; + + scriptdir = get_extension_script_directory(control); + + result = (char *) palloc(MAXPGPATH); + snprintf(result, MAXPGPATH, "%s/%s%c%s.control", + scriptdir, control->name, EXT_VERSION_SEP, version); + + pfree(scriptdir); + + return result; +} + +static char * +get_extension_script_filename(ExtensionControlFile *control, + const char *from_version, const char *version) +{ + char *result; + char *scriptdir; + + scriptdir = get_extension_script_directory(control); + + result = (char *) palloc(MAXPGPATH); + if (from_version) + snprintf(result, MAXPGPATH, "%s/%s%c%s%c%s.sql", + scriptdir, control->name, EXT_VERSION_SEP, from_version, + EXT_VERSION_SEP, version); + else + snprintf(result, MAXPGPATH, "%s/%s%c%s.sql", + scriptdir, control->name, EXT_VERSION_SEP, version); + + pfree(scriptdir); return result; } /* - * Read the control file for the specified extension. + * Parse contents of primary or auxiliary control file, and fill in + * fields of *control. We parse primary file if version == NULL, + * else the optional auxiliary file for that version. * - * The control file is supposed to be very short, half a dozen lines, and - * reading it is only allowed to superuser, so we don't worry about - * memory allocation risks here. Also note that we don't worry about - * what encoding it's in; all values are expected to be ASCII. + * Control files are supposed to be very short, half a dozen lines, + * so we don't worry about memory allocation risks here. Also we don't + * worry about what encoding it's in; all values are expected to be ASCII. */ -static ExtensionControlFile * -read_extension_control_file(const char *extname) +static void +parse_extension_control_file(ExtensionControlFile *control, + const char *version) { - char *filename = get_extension_control_filename(extname); + char *filename; FILE *file; - ExtensionControlFile *control; ConfigVariable *item, *head = NULL, *tail = NULL; /* - * Parse the file content, using GUC's file parsing code + * Locate the file to read. Auxiliary files are optional. */ + if (version) + filename = get_extension_aux_control_filename(control, version); + else + filename = get_extension_control_filename(control->name); + if ((file = AllocateFile(filename, "r")) == NULL) + { + if (version && errno == ENOENT) + { + /* no auxiliary file for this version */ + pfree(filename); + return; + } ereport(ERROR, (errcode_for_file_access(), errmsg("could not open extension control file \"%s\": %m", filename))); + } + /* + * Parse the file content, using GUC's file parsing code + */ ParseConfigFp(file, filename, 0, ERROR, &head, &tail); FreeFile(file); - /* - * Set up default values. Pointer fields are initially null. - */ - control = (ExtensionControlFile *) palloc0(sizeof(ExtensionControlFile)); - control->name = pstrdup(extname); - control->relocatable = false; - control->encoding = -1; - /* * Convert the ConfigVariable list into ExtensionControlFile entries. */ for (item = head; item != NULL; item = item->next) { - if (strcmp(item->name, "script") == 0) + if (strcmp(item->name, "directory") == 0) { - control->script = pstrdup(item->value); + if (version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("parameter \"%s\" cannot be set in a per-version extension control file", + item->name))); + + control->directory = pstrdup(item->value); } - else if (strcmp(item->name, "version") == 0) + else if (strcmp(item->name, "default_version") == 0) { - control->version = pstrdup(item->value); + if (version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("parameter \"%s\" cannot be set in a per-version extension control file", + item->name))); + + control->default_version = pstrdup(item->value); } else if (strcmp(item->name, "comment") == 0) { @@ -324,6 +454,12 @@ read_extension_control_file(const char *extname) } else if (strcmp(item->name, "encoding") == 0) { + if (version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("parameter \"%s\" cannot be set in a per-version extension control file", + item->name))); + control->encoding = pg_valid_server_encoding(item->value); if (control->encoding < 0) ereport(ERROR, @@ -360,22 +496,35 @@ read_extension_control_file(const char *extname) (errcode(ERRCODE_SYNTAX_ERROR), errmsg("parameter \"schema\" cannot be specified when \"relocatable\" is true"))); + pfree(filename); +} + +/* + * Read the primary control file for the specified extension. + */ +static ExtensionControlFile * +read_extension_control_file(const char *extname) +{ + ExtensionControlFile *control; + /* - * script defaults to ${extension-name}.sql + * Set up default values. Pointer fields are initially null. */ - if (control->script == NULL) - { - char script[MAXPGPATH]; + control = (ExtensionControlFile *) palloc0(sizeof(ExtensionControlFile)); + control->name = pstrdup(extname); + control->relocatable = false; + control->encoding = -1; - snprintf(script, MAXPGPATH, "%s.sql", control->name); - control->script = pstrdup(script); - } + /* + * Parse the primary control file. + */ + parse_extension_control_file(control, NULL); return control; } /* - * Read the SQL script into a string, and convert to database encoding + * Read a SQL script file into a string, and convert to database encoding */ static char * read_extension_script_file(const ExtensionControlFile *control, @@ -513,20 +662,26 @@ execute_sql_string(const char *sql, const char *filename) } /* - * Execute the extension's script file + * Execute the appropriate script file for installing or updating the extension + * + * If from_version isn't NULL, it's an update */ static void execute_extension_script(Oid extensionOid, ExtensionControlFile *control, + const char *from_version, + const char *version, List *requiredSchemas, const char *schemaName, Oid schemaOid) { - char *filename = get_extension_absolute_path(control->script); + char *filename; char *save_client_min_messages, *save_log_min_messages, *save_search_path; StringInfoData pathbuf; ListCell *lc; + filename = get_extension_script_filename(control, from_version, version); + /* * Force client_min_messages and log_min_messages to be at least WARNING, * so that we won't spam the user with useless NOTICE messages from common @@ -635,6 +790,186 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, GUC_ACTION_LOCAL, true); } +/* + * Find or create an ExtensionVersionInfo for the specified version name + * + * Currently, we just use a List of the ExtensionVersionInfo's. Searching + * for them therefore uses about O(N^2) time when there are N versions of + * the extension. We could change the data structure to a hash table if + * this ever becomes a bottleneck. + */ +static ExtensionVersionInfo * +get_ext_ver_info(const char *versionname, List **evi_list) +{ + ExtensionVersionInfo *evi; + ListCell *lc; + + foreach(lc, *evi_list) + { + evi = (ExtensionVersionInfo *) lfirst(lc); + if (strcmp(evi->name, versionname) == 0) + return evi; + } + + evi = (ExtensionVersionInfo *) palloc(sizeof(ExtensionVersionInfo)); + evi->name = pstrdup(versionname); + evi->reachable = NIL; + /* initialize for later application of Dijkstra's algorithm */ + evi->distance_known = false; + evi->distance = INT_MAX; + evi->previous = NULL; + + *evi_list = lappend(*evi_list, evi); + + return evi; +} + +/* + * Locate the nearest unprocessed ExtensionVersionInfo + * + * This part of the algorithm is also about O(N^2). A priority queue would + * make it much faster, but for now there's no need. + */ +static ExtensionVersionInfo * +get_nearest_unprocessed_vertex(List *evi_list) +{ + ExtensionVersionInfo *evi = NULL; + ListCell *lc; + + foreach(lc, evi_list) + { + ExtensionVersionInfo *evi2 = (ExtensionVersionInfo *) lfirst(lc); + + /* only vertices whose distance is still uncertain are candidates */ + if (evi2->distance_known) + continue; + /* remember the closest such vertex */ + if (evi == NULL || + evi->distance > evi2->distance) + evi = evi2; + } + + return evi; +} + +/* + * Obtain information about the set of update scripts available for the + * specified extension. The result is a List of ExtensionVersionInfo + * structs, each with a subsidiary list of the ExtensionVersionInfos for + * the versions that can be reached in one step from that version. + */ +static List * +get_ext_ver_list(ExtensionControlFile *control) +{ + List *evi_list = NIL; + int extnamelen = strlen(control->name); + char *location; + DIR *dir; + struct dirent *de; + + location = get_extension_script_directory(control); + dir = AllocateDir(location); + while ((de = ReadDir(dir, location)) != NULL) + { + char *vername; + char *vername2; + ExtensionVersionInfo *evi; + ExtensionVersionInfo *evi2; + + /* must be a .sql file ... */ + if (!is_extension_script_filename(de->d_name)) + continue; + + /* ... matching extension name followed by separator */ + if (strncmp(de->d_name, control->name, extnamelen) != 0 || + de->d_name[extnamelen] != EXT_VERSION_SEP) + continue; + + /* extract version names from 'extname-something.sql' filename */ + vername = pstrdup(de->d_name + extnamelen + 1); + *strrchr(vername, '.') = '\0'; + vername2 = strchr(vername, EXT_VERSION_SEP); + if (!vername2) + continue; /* it's not an update script */ + *vername2++ = '\0'; + + /* Create ExtensionVersionInfos and link them together */ + evi = get_ext_ver_info(vername, &evi_list); + evi2 = get_ext_ver_info(vername2, &evi_list); + evi->reachable = lappend(evi->reachable, evi2); + } + FreeDir(dir); + + return evi_list; +} + +/* + * Given an initial and final version name, identify the sequence of update + * scripts that have to be applied to perform that update. + * + * Result is a List of names of versions to transition through. + */ +static List * +identify_update_path(ExtensionControlFile *control, + const char *oldVersion, const char *newVersion) +{ + List *result; + List *evi_list; + ExtensionVersionInfo *evi_start; + ExtensionVersionInfo *evi_target; + ExtensionVersionInfo *evi; + + if (strcmp(oldVersion, newVersion) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("version to install or update to must be different from old version"))); + + /* Extract the version update graph from the script directory */ + evi_list = get_ext_ver_list(control); + + /* Initialize start and end vertices */ + evi_start = get_ext_ver_info(oldVersion, &evi_list); + evi_target = get_ext_ver_info(newVersion, &evi_list); + + evi_start->distance = 0; + + while ((evi = get_nearest_unprocessed_vertex(evi_list)) != NULL) + { + ListCell *lc; + + if (evi->distance == INT_MAX) + break; /* all remaining vertices are unreachable */ + evi->distance_known = true; + if (evi == evi_target) + break; /* found shortest path to target */ + foreach(lc, evi->reachable) + { + ExtensionVersionInfo *evi2 = (ExtensionVersionInfo *) lfirst(lc); + int newdist; + + newdist = evi->distance + 1; + if (newdist < evi2->distance) + { + evi2->distance = newdist; + evi2->previous = evi; + } + } + } + + if (!evi_target->distance_known) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension \"%s\" has no update path from version \"%s\" to version \"%s\"", + control->name, oldVersion, newVersion))); + + /* Build and return list of version names representing the update path */ + result = NIL; + for (evi = evi_target; evi != evi_start; evi = evi->previous) + result = lcons(evi->name, result); + + return result; +} + /* * CREATE EXTENSION */ @@ -642,8 +977,12 @@ void CreateExtension(CreateExtensionStmt *stmt) { DefElem *d_schema = NULL; + DefElem *d_new_version = NULL; + DefElem *d_old_version = NULL; char *schemaName; Oid schemaOid; + char *versionName; + char *oldVersionName; Oid extowner = GetUserId(); ExtensionControlFile *control; List *requiredExtensions; @@ -668,6 +1007,9 @@ CreateExtension(CreateExtensionStmt *stmt) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("nested CREATE EXTENSION is not supported"))); + /* Check extension name validity before any filesystem access */ + check_valid_extension_name(stmt->extname); + /* * Check for duplicate extension name. The unique index on * pg_extension.extname would catch this anyway, and serves as a backstop @@ -679,9 +1021,9 @@ CreateExtension(CreateExtensionStmt *stmt) errmsg("extension \"%s\" already exists", stmt->extname))); /* - * Read the control file. Note we assume that it does not contain - * any non-ASCII data, so there is no need to worry about encoding - * at this point. + * Read the primary control file. Note we assume that it does not contain + * any non-ASCII data, so there is no need to worry about encoding at this + * point. */ control = read_extension_control_file(stmt->extname); @@ -700,10 +1042,58 @@ CreateExtension(CreateExtensionStmt *stmt) errmsg("conflicting or redundant options"))); d_schema = defel; } + else if (strcmp(defel->defname, "new_version") == 0) + { + if (d_new_version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + d_new_version = defel; + } + else if (strcmp(defel->defname, "old_version") == 0) + { + if (d_old_version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + d_old_version = defel; + } else elog(ERROR, "unrecognized option: %s", defel->defname); } + /* + * Determine the version to install + */ + if (d_new_version && d_new_version->arg) + versionName = strVal(d_new_version->arg); + else if (control->default_version) + versionName = control->default_version; + else + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("version to install must be specified"))); + versionName = NULL; /* keep compiler quiet */ + } + check_valid_version_name(versionName); + + /* + * Modify control parameters for specific new version + */ + parse_extension_control_file(control, versionName); + + /* + * Determine the (unpackaged) version to update from, if any + */ + if (d_old_version && d_old_version->arg) + { + oldVersionName = strVal(d_old_version->arg); + check_valid_version_name(oldVersionName); + } + else + oldVersionName = NULL; + /* * Determine the target schema to install the extension into */ @@ -799,7 +1189,7 @@ CreateExtension(CreateExtensionStmt *stmt) */ extensionOid = InsertExtensionTuple(control->name, extowner, schemaOid, control->relocatable, - control->version, + versionName, PointerGetDatum(NULL), PointerGetDatum(NULL), requiredExtensions); @@ -811,10 +1201,36 @@ CreateExtension(CreateExtensionStmt *stmt) CreateComments(extensionOid, ExtensionRelationId, 0, control->comment); /* - * Finally, execute the extension script to create the member objects + * Finally, execute the extension's script file(s) */ - execute_extension_script(extensionOid, control, requiredSchemas, - schemaName, schemaOid); + if (oldVersionName == NULL) + { + /* Simple install */ + execute_extension_script(extensionOid, control, + oldVersionName, versionName, + requiredSchemas, + schemaName, schemaOid); + } + else + { + /* Update from unpackaged objects --- find update-file path */ + List *updateVersions; + + updateVersions = identify_update_path(control, + oldVersionName, + versionName); + + foreach(lc, updateVersions) + { + char *vname = (char *) lfirst(lc); + + execute_extension_script(extensionOid, control, + oldVersionName, vname, + requiredSchemas, + schemaName, schemaOid); + oldVersionName = vname; + } + } } /* @@ -858,12 +1274,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner, values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner); values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid); values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable); - - if (extVersion == NULL) - nulls[Anum_pg_extension_extversion - 1] = true; - else - values[Anum_pg_extension_extversion - 1] = - CStringGetTextDatum(extVersion); + values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion); if (extConfig == PointerGetDatum(NULL)) nulls[Anum_pg_extension_extconfig - 1] = true; @@ -1102,11 +1513,11 @@ pg_available_extensions(PG_FUNCTION_ARGS) /* name */ values[0] = DirectFunctionCall1(namein, CStringGetDatum(control->name)); - /* version */ - if (control->version == NULL) + /* default_version */ + if (control->default_version == NULL) nulls[1] = true; else - values[1] = CStringGetTextDatum(control->version); + values[1] = CStringGetTextDatum(control->default_version); /* relocatable */ values[2] = BoolGetDatum(control->relocatable); /* comment */ @@ -1435,6 +1846,230 @@ AlterExtensionNamespace(List *names, const char *newschema) NamespaceRelationId, oldNspOid, nspOid); } +/* + * Execute ALTER EXTENSION UPDATE + */ +void +ExecAlterExtensionStmt(AlterExtensionStmt *stmt) +{ + DefElem *d_new_version = NULL; + char *schemaName; + Oid schemaOid; + char *versionName; + char *oldVersionName; + ExtensionControlFile *control; + List *requiredExtensions; + List *requiredSchemas; + Oid extensionOid; + Relation extRel; + ScanKeyData key[1]; + SysScanDesc extScan; + HeapTuple extTup; + Form_pg_extension extForm; + Datum values[Natts_pg_extension]; + bool nulls[Natts_pg_extension]; + bool repl[Natts_pg_extension]; + List *updateVersions; + ObjectAddress myself; + Datum datum; + bool isnull; + ListCell *lc; + + /* + * For now, insist on superuser privilege. Later we might want to + * relax this to ownership of the extension. + */ + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("must be superuser to use ALTER EXTENSION")))); + + /* + * We use global variables to track the extension being created, so we + * can create/alter only one extension at the same time. + */ + if (creating_extension) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("nested ALTER EXTENSION is not supported"))); + + /* Look up the extension --- it must already exist in pg_extension */ + extRel = heap_open(ExtensionRelationId, RowExclusiveLock); + + ScanKeyInit(&key[0], + Anum_pg_extension_extname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->extname)); + + extScan = systable_beginscan(extRel, ExtensionNameIndexId, true, + SnapshotNow, 1, key); + + extTup = systable_getnext(extScan); + + if (!HeapTupleIsValid(extTup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("extension \"%s\" does not exist", + stmt->extname))); + + /* Copy tuple so we can modify it below */ + extTup = heap_copytuple(extTup); + extForm = (Form_pg_extension) GETSTRUCT(extTup); + extensionOid = HeapTupleGetOid(extTup); + + systable_endscan(extScan); + + /* + * Read the primary control file. Note we assume that it does not contain + * any non-ASCII data, so there is no need to worry about encoding at this + * point. + */ + control = read_extension_control_file(stmt->extname); + + /* + * Read the statement option list + */ + foreach(lc, stmt->options) + { + DefElem *defel = (DefElem *) lfirst(lc); + + if (strcmp(defel->defname, "new_version") == 0) + { + if (d_new_version) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + d_new_version = defel; + } + else + elog(ERROR, "unrecognized option: %s", defel->defname); + } + + /* + * Determine the version to install + */ + if (d_new_version && d_new_version->arg) + versionName = strVal(d_new_version->arg); + else if (control->default_version) + versionName = control->default_version; + else + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("version to install must be specified"))); + versionName = NULL; /* keep compiler quiet */ + } + check_valid_version_name(versionName); + + /* + * Modify control parameters for specific new version + */ + parse_extension_control_file(control, versionName); + + /* + * Determine the existing version we are upgrading from + */ + datum = heap_getattr(extTup, Anum_pg_extension_extversion, + RelationGetDescr(extRel), &isnull); + if (isnull) + elog(ERROR, "extversion is null"); + oldVersionName = text_to_cstring(DatumGetTextPP(datum)); + + /* + * Determine the target schema (already set by original install) + */ + schemaOid = extForm->extnamespace; + schemaName = get_namespace_name(schemaOid); + + /* + * Look up the prerequisite extensions, and build lists of their OIDs + * and the OIDs of their target schemas. We assume that the requires + * list is version-specific, so the dependencies can change across + * versions. But note that only the final version's requires list + * is being consulted here! + */ + requiredExtensions = NIL; + requiredSchemas = NIL; + foreach(lc, control->requires) + { + char *curreq = (char *) lfirst(lc); + Oid reqext; + Oid reqschema; + + /* + * We intentionally don't use get_extension_oid's default error + * message here, because it would be confusing in this context. + */ + reqext = get_extension_oid(curreq, true); + if (!OidIsValid(reqext)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("required extension \"%s\" is not installed", + curreq))); + reqschema = get_extension_schema(reqext); + requiredExtensions = lappend_oid(requiredExtensions, reqext); + requiredSchemas = lappend_oid(requiredSchemas, reqschema); + } + + /* + * Modify extversion in the pg_extension tuple + */ + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + memset(repl, 0, sizeof(repl)); + + values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(versionName); + repl[Anum_pg_extension_extversion - 1] = true; + + extTup = heap_modify_tuple(extTup, RelationGetDescr(extRel), + values, nulls, repl); + + simple_heap_update(extRel, &extTup->t_self, extTup); + CatalogUpdateIndexes(extRel, extTup); + + heap_close(extRel, RowExclusiveLock); + + /* + * Remove and recreate dependencies on prerequisite extensions + */ + deleteDependencyRecordsForClass(ExtensionRelationId, extensionOid, + ExtensionRelationId, DEPENDENCY_NORMAL); + + myself.classId = ExtensionRelationId; + myself.objectId = extensionOid; + myself.objectSubId = 0; + + foreach(lc, requiredExtensions) + { + Oid reqext = lfirst_oid(lc); + ObjectAddress otherext; + + otherext.classId = ExtensionRelationId; + otherext.objectId = reqext; + otherext.objectSubId = 0; + + recordDependencyOn(&myself, &otherext, DEPENDENCY_NORMAL); + } + + /* + * Finally, execute the extension's script file(s) + */ + updateVersions = identify_update_path(control, + oldVersionName, + versionName); + + foreach(lc, updateVersions) + { + char *vname = (char *) lfirst(lc); + + execute_extension_script(extensionOid, control, + oldVersionName, vname, + requiredSchemas, + schemaName, schemaOid); + oldVersionName = vname; + } +} + /* * Execute ALTER EXTENSION ADD/DROP */ diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 46acaf8d701abc6be9b8683f30a3f04d066f596c..57d580208102c116323ffd9ff74dcba21fcd3ecc 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3251,6 +3251,17 @@ _copyCreateExtensionStmt(CreateExtensionStmt *from) return newnode; } +static AlterExtensionStmt * +_copyAlterExtensionStmt(AlterExtensionStmt *from) +{ + AlterExtensionStmt *newnode = makeNode(AlterExtensionStmt); + + COPY_STRING_FIELD(extname); + COPY_NODE_FIELD(options); + + return newnode; +} + static AlterExtensionContentsStmt * _copyAlterExtensionContentsStmt(AlterExtensionContentsStmt *from) { @@ -4267,6 +4278,9 @@ copyObject(void *from) case T_CreateExtensionStmt: retval = _copyCreateExtensionStmt(from); break; + case T_AlterExtensionStmt: + retval = _copyAlterExtensionStmt(from); + break; case T_AlterExtensionContentsStmt: retval = _copyAlterExtensionContentsStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 2fbe99937d2e49d901058e43118a978d99201ffc..f57cf99ba2c1f473cbef55c7c46f78765ceb1ae3 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1654,6 +1654,15 @@ _equalCreateExtensionStmt(CreateExtensionStmt *a, CreateExtensionStmt *b) return true; } +static bool +_equalAlterExtensionStmt(AlterExtensionStmt *a, AlterExtensionStmt *b) +{ + COMPARE_STRING_FIELD(extname); + COMPARE_NODE_FIELD(options); + + return true; +} + static bool _equalAlterExtensionContentsStmt(AlterExtensionContentsStmt *a, AlterExtensionContentsStmt *b) { @@ -2869,6 +2878,9 @@ equal(void *a, void *b) case T_CreateExtensionStmt: retval = _equalCreateExtensionStmt(a, b); break; + case T_AlterExtensionStmt: + retval = _equalAlterExtensionStmt(a, b); + break; case T_AlterExtensionContentsStmt: retval = _equalAlterExtensionContentsStmt(a, b); break; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 82ff9accc72761c83afa5b6d7e6fc9cb2e1d8e9a..a99f8c6ca248168362834f2198f4475ca15e791a 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -185,7 +185,7 @@ static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_ AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt AlterFdwStmt AlterForeignServerStmt AlterGroupStmt AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterTableStmt - AlterExtensionContentsStmt AlterForeignTableStmt + AlterExtensionStmt AlterExtensionContentsStmt AlterForeignTableStmt AlterCompositeTypeStmt AlterUserStmt AlterUserMappingStmt AlterUserSetStmt AlterRoleStmt AlterRoleSetStmt AlterDefaultPrivilegesStmt DefACLAction @@ -227,9 +227,11 @@ static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_ %type <dbehavior> opt_drop_behavior %type <list> createdb_opt_list alterdb_opt_list copy_opt_list - transaction_mode_list create_extension_opt_list + transaction_mode_list + create_extension_opt_list alter_extension_opt_list %type <defelt> createdb_opt_item alterdb_opt_item copy_opt_item - transaction_mode_item create_extension_opt_item + transaction_mode_item + create_extension_opt_item alter_extension_opt_item %type <ival> opt_lock lock_type cast_context %type <ival> vacuum_option_list vacuum_option_elem @@ -664,6 +666,7 @@ stmt : | AlterDefaultPrivilegesStmt | AlterDomainStmt | AlterEnumStmt + | AlterExtensionStmt | AlterExtensionContentsStmt | AlterFdwStmt | AlterForeignServerStmt @@ -3222,7 +3225,7 @@ DropTableSpaceStmt: DROP TABLESPACE name * * QUERY: * CREATE EXTENSION extension - * [ WITH ] [ SCHEMA [=] schema ] + * [ WITH ] [ SCHEMA schema ] [ VERSION version ] [ FROM oldversion ] * *****************************************************************************/ @@ -3243,9 +3246,46 @@ create_extension_opt_list: ; create_extension_opt_item: - SCHEMA opt_equal name + SCHEMA name { - $$ = makeDefElem("schema", (Node *)makeString($3)); + $$ = makeDefElem("schema", (Node *)makeString($2)); + } + | VERSION_P ColId_or_Sconst + { + $$ = makeDefElem("new_version", (Node *)makeString($2)); + } + | FROM ColId_or_Sconst + { + $$ = makeDefElem("old_version", (Node *)makeString($2)); + } + ; + +/***************************************************************************** + * + * ALTER EXTENSION name UPDATE [ TO version ] + * + *****************************************************************************/ + +AlterExtensionStmt: ALTER EXTENSION name UPDATE alter_extension_opt_list + { + AlterExtensionStmt *n = makeNode(AlterExtensionStmt); + n->extname = $3; + n->options = $5; + $$ = (Node *) n; + } + ; + +alter_extension_opt_list: + alter_extension_opt_list alter_extension_opt_item + { $$ = lappend($1, $2); } + | /* EMPTY */ + { $$ = NIL; } + ; + +alter_extension_opt_item: + TO ColId_or_Sconst + { + $$ = makeDefElem("new_version", (Node *)makeString($2)); } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index c942de3bf62cb7b47ab26c1e2b62ff78d808945a..8ca042024f34ef3408f80d430f864915c8b007c1 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -212,6 +212,7 @@ check_xact_readonly(Node *parsetree) case T_AlterTSDictionaryStmt: case T_AlterTSConfigurationStmt: case T_CreateExtensionStmt: + case T_AlterExtensionStmt: case T_AlterExtensionContentsStmt: case T_CreateFdwStmt: case T_AlterFdwStmt: @@ -601,6 +602,10 @@ standard_ProcessUtility(Node *parsetree, CreateExtension((CreateExtensionStmt *) parsetree); break; + case T_AlterExtensionStmt: + ExecAlterExtensionStmt((AlterExtensionStmt *) parsetree); + break; + case T_AlterExtensionContentsStmt: ExecAlterExtensionContentsStmt((AlterExtensionContentsStmt *) parsetree); break; @@ -1680,6 +1685,10 @@ CreateCommandTag(Node *parsetree) tag = "CREATE EXTENSION"; break; + case T_AlterExtensionStmt: + tag = "ALTER EXTENSION"; + break; + case T_AlterExtensionContentsStmt: tag = "ALTER EXTENSION"; break; @@ -2307,6 +2316,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_CreateExtensionStmt: + case T_AlterExtensionStmt: case T_AlterExtensionContentsStmt: lev = LOGSTMT_DDL; break; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 5561c7a6876baa5ff110a6b9bc9429e11679d4db..83c7157b2e8b99ed95f630415a3b81a3c6a5ee29 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -2725,10 +2725,7 @@ getExtensions(int *numExtensions) extinfo[i].dobj.name = strdup(PQgetvalue(res, i, i_extname)); extinfo[i].namespace = strdup(PQgetvalue(res, i, i_nspname)); extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't'; - if (PQgetisnull(res, i, i_extversion)) - extinfo[i].extversion = NULL; - else - extinfo[i].extversion = strdup(PQgetvalue(res, i, i_extversion)); + extinfo[i].extversion = strdup(PQgetvalue(res, i, i_extversion)); extinfo[i].extconfig = strdup(PQgetvalue(res, i, i_extconfig)); extinfo[i].extcondition = strdup(PQgetvalue(res, i, i_extcondition)); } @@ -6942,10 +6939,7 @@ dumpExtension(Archive *fout, ExtensionInfo *extinfo) appendStringLiteralAH(q, extinfo->namespace, fout); appendPQExpBuffer(q, ", "); appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false"); - if (extinfo->extversion) - appendStringLiteralAH(q, extinfo->extversion, fout); - else - appendPQExpBuffer(q, "NULL"); + appendStringLiteralAH(q, extinfo->extversion, fout); appendPQExpBuffer(q, ", "); /* * Note that we're pushing extconfig (an OID array) back into diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 2c656068f827db45bdb04d0792d79dcdff2072b3..a31281e431cbfeedf4d79fae9339c6559f7e60f5 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -868,7 +868,7 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev2_wd, "EXTENSION") == 0) { static const char *const list_ALTEREXTENSION[] = - {"ADD", "DROP", "SET SCHEMA", NULL}; + {"ADD", "DROP", "UPDATE", "SET SCHEMA", NULL}; COMPLETE_WITH_LIST(list_ALTEREXTENSION); } diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h index 0ad47b01a4b0a8595b6d8cec13797074f5b5d228..63a1a0711f927125442d25815ab84add5dcf9152 100644 --- a/src/include/catalog/pg_extension.h +++ b/src/include/catalog/pg_extension.h @@ -36,9 +36,11 @@ CATALOG(pg_extension,3079) bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */ /* - * VARIABLE LENGTH FIELDS start here. These fields may be NULL, too. + * VARIABLE LENGTH FIELDS start here. + * + * extversion should never be null, but the others can be. */ - text extversion; /* extension version ID, if any */ + text extversion; /* extension version name */ Oid extconfig[1]; /* dumpable configuration tables */ text extcondition[1]; /* WHERE clauses for config tables */ } FormData_pg_extension; diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 7c94449a6cb0f5e853fe70bd79ae7ce0c87b5069..c6e69d5fd4216a0b3965b5f1df7251472d25bb2e 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -37,6 +37,8 @@ extern Oid InsertExtensionTuple(const char *extName, Oid extOwner, Datum extConfig, Datum extCondition, List *requiredExtensions); +extern void ExecAlterExtensionStmt(AlterExtensionStmt *stmt); + extern void ExecAlterExtensionContentsStmt(AlterExtensionContentsStmt *stmt); extern Oid get_extension_oid(const char *extname, bool missing_ok); diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 15bf0631e44c50fb2a94014fd02edf997adcb2b7..e0d05748dafb4d5c223bcc4cb2c17891b43f1d6e 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -356,6 +356,7 @@ typedef enum NodeTag T_SecLabelStmt, T_CreateForeignTableStmt, T_CreateExtensionStmt, + T_AlterExtensionStmt, T_AlterExtensionContentsStmt, /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b54f0cfe02f7bb5d47d4c37171d12e0543cdbe1e..1aa3e913b5484446b8377d03cde779d98c4fde0d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1546,6 +1546,14 @@ typedef struct CreateExtensionStmt List *options; /* List of DefElem nodes */ } CreateExtensionStmt; +/* Only used for ALTER EXTENSION UPDATE; later might need an action field */ +typedef struct AlterExtensionStmt +{ + NodeTag type; + char *extname; + List *options; /* List of DefElem nodes */ +} AlterExtensionStmt; + typedef struct AlterExtensionContentsStmt { NodeTag type;