</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
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
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>
<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>
<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>
<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>
</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>
</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>
</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 );
<programlisting>
# pair extension
comment = 'A key/value pair data type'
-version = '0.1.2'
+default_version = '1.0'
relocatable = true
</programlisting>
</para>
<programlisting>
EXTENSION = pair
-DATA = pair.sql
+DATA = pair-1.0.sql
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --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
#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? */
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
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
*/
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)
{
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)
{
}
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,
(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,
}
/*
- * 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
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
*/
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;
(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
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);
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
*/
*/
extensionOid = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
- control->version,
+ versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
requiredExtensions);
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;
+ }
+ }
}
/*
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;
/* 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 */
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
*/