diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml index 394aa87900286a1bc289fc9ff6327b89ffa33c27..50924a78f0e79da1c0bc264d0aa692721698dbe9 100644 --- a/doc/src/sgml/extend.sgml +++ b/doc/src/sgml/extend.sgml @@ -368,8 +368,8 @@ installation's <literal>SHAREDIR/extension</literal> directory. There 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 + <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/extension</literal> directory; but the control file can specify a different directory for the script file(s). @@ -378,7 +378,7 @@ <para> The file format for an extension control file is the same as for the <filename>postgresql.conf</> file, namely a list of - <replaceable>parameter-name</> <literal>=</> <replaceable>value</> + <replaceable>parameter_name</> <literal>=</> <replaceable>value</> assignments, one per line. Blank lines and comments introduced by <literal>#</> are allowed. Be sure to quote any value that is not a single word or number. @@ -477,7 +477,7 @@ 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>. + <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 @@ -671,15 +671,15 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr 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 + <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 + <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 @@ -712,7 +712,7 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr 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>. + <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 @@ -723,7 +723,7 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr <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 + <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. @@ -734,11 +734,13 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr 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. + (A version name can actually be any string that doesn't contain + <literal>--</> or leading or trailing <literal>-</>.) </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 + 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 @@ -761,7 +763,7 @@ SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entr </para> <para> - The script file <filename>pair-1.0.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 ); @@ -803,7 +805,7 @@ relocatable = true <programlisting> EXTENSION = pair -DATA = pair-1.0.sql +DATA = pair--1.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) @@ -860,7 +862,7 @@ include $(PGXS) <programlisting> MODULES = isbn_issn EXTENSION = isbn_issn -DATA_built = isbn_issn-1.0.sql +DATA = isbn_issn--1.0.sql DOCS = README.isbn_issn PG_CONFIG = pg_config diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 99aface408f29a130a80b8d5884a394790d2835a..0661303fea3bf330fc30fc5d7c8655dcb8a73b46 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -57,9 +57,6 @@ 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 */ @@ -225,9 +222,42 @@ get_extension_schema(Oid ext_oid) static void check_valid_extension_name(const char *extensionname) { + int namelen = strlen(extensionname); + /* - * No directory separators (this is sufficient to prevent ".." style - * attacks). + * Disallow empty names (the parser rejects empty identifiers anyway, + * but let's check). + */ + if (namelen == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension name: \"%s\"", extensionname), + errdetail("Extension names must not be empty."))); + + /* + * No double dashes, since that would make script filenames ambiguous. + */ + if (strstr(extensionname, "--")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension name: \"%s\"", extensionname), + errdetail("Extension names must not contain \"--\"."))); + + /* + * No leading or trailing dash either. (We could probably allow this, + * but it would require much care in filename parsing and would make + * filenames visually if not formally ambiguous. Since there's no + * real-world use case, let's just forbid it.) + */ + if (extensionname[0] == '-' || extensionname[namelen - 1] == '-') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension name: \"%s\"", extensionname), + errdetail("Extension names must not begin or end with \"-\"."))); + + /* + * No directory separators either (this is sufficient to prevent ".." + * style attacks). */ if (first_dir_separator(extensionname) != NULL) ereport(ERROR, @@ -239,16 +269,39 @@ check_valid_extension_name(const char *extensionname) 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)) + int namelen = strlen(versionname); + + /* + * Disallow empty names (we could possibly allow this, but there seems + * little point). + */ + if (namelen == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension version name: \"%s\"", versionname), + errdetail("Version names must not be empty."))); + + /* + * No double dashes, since that would make script filenames ambiguous. + */ + if (strstr(versionname, "--")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid extension version name: \"%s\"", versionname), + errdetail("Version names must not contain \"--\"."))); + + /* + * No leading or trailing dash either. + */ + if (versionname[0] == '-' || versionname[namelen - 1] == '-') 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))); + errdetail("Version names must not begin or end with \"-\"."))); + /* - * No directory separators (this is sufficient to prevent ".." style - * attacks). + * No directory separators either (this is sufficient to prevent ".." + * style attacks). */ if (first_dir_separator(versionname) != NULL) ereport(ERROR, @@ -336,8 +389,8 @@ get_extension_aux_control_filename(ExtensionControlFile *control, 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); + snprintf(result, MAXPGPATH, "%s/%s--%s.control", + scriptdir, control->name, version); pfree(scriptdir); @@ -355,12 +408,11 @@ get_extension_script_filename(ExtensionControlFile *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); + snprintf(result, MAXPGPATH, "%s/%s--%s--%s.sql", + scriptdir, control->name, from_version, version); else - snprintf(result, MAXPGPATH, "%s/%s%c%s.sql", - scriptdir, control->name, EXT_VERSION_SEP, version); + snprintf(result, MAXPGPATH, "%s/%s--%s.sql", + scriptdir, control->name, version); pfree(scriptdir); @@ -426,7 +478,7 @@ parse_extension_control_file(ExtensionControlFile *control, if (version) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("parameter \"%s\" cannot be set in a per-version extension control file", + errmsg("parameter \"%s\" cannot be set in a secondary extension control file", item->name))); control->directory = pstrdup(item->value); @@ -436,7 +488,7 @@ parse_extension_control_file(ExtensionControlFile *control, if (version) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("parameter \"%s\" cannot be set in a per-version extension control file", + errmsg("parameter \"%s\" cannot be set in a secondary extension control file", item->name))); control->default_version = pstrdup(item->value); @@ -907,16 +959,18 @@ get_ext_ver_list(ExtensionControlFile *control) /* ... matching extension name followed by separator */ if (strncmp(de->d_name, control->name, extnamelen) != 0 || - de->d_name[extnamelen] != EXT_VERSION_SEP) + de->d_name[extnamelen] != '-' || + de->d_name[extnamelen + 1] != '-') continue; - /* extract version names from 'extname-something.sql' filename */ - vername = pstrdup(de->d_name + extnamelen + 1); + /* extract version names from 'extname--something.sql' filename */ + vername = pstrdup(de->d_name + extnamelen + 2); *strrchr(vername, '.') = '\0'; - vername2 = strchr(vername, EXT_VERSION_SEP); + vername2 = strstr(vername, "--"); if (!vername2) continue; /* it's not an update script */ - *vername2++ = '\0'; + *vername2 = '\0'; /* terminate first version */ + vername2 += 2; /* and point to second */ /* Create ExtensionVersionInfos and link them together */ evi = get_ext_ver_info(vername, &evi_list); @@ -979,6 +1033,20 @@ identify_update_path(ExtensionControlFile *control, evi2->distance = newdist; evi2->previous = evi; } + else if (newdist == evi2->distance && + evi2->previous != NULL && + strcmp(evi->name, evi2->previous->name) < 0) + { + /* + * Break ties in favor of the version name that comes first + * according to strcmp(). This behavior is undocumented and + * users shouldn't rely on it. We do it just to ensure that + * if there is a tie, the update path that is chosen does not + * depend on random factors like the order in which directory + * entries get visited. + */ + evi2->previous = evi; + } } } @@ -1251,7 +1319,7 @@ CreateExtension(CreateExtensionStmt *stmt) requiredExtensions); /* - * Apply any comment on extension + * Apply any control-file comment on extension */ if (control->comment != NULL) CreateComments(extensionOid, ExtensionRelationId, 0, control->comment); @@ -1544,6 +1612,10 @@ pg_available_extensions(PG_FUNCTION_ARGS) extname = pstrdup(de->d_name); *strrchr(extname, '.') = '\0'; + /* ignore it if it's an auxiliary control file */ + if (strstr(extname, "--")) + continue; + control = read_extension_control_file(extname); memset(values, 0, sizeof(values));