Skip to content

Commit 72a5b1f

Browse files
committed
Add @extschema:name@ and no_relocate options to extensions.
@extschema:name@ extends the existing @extschema@ feature so that we can also insert the schema name of some required extension, thus making cross-extension references robust even if they are in different schemas. However, this has the same hazard as @extschema@: if the schema name is embedded literally in an installed object, rather than being looked up once during extension script execution, then it's no longer safe to relocate the other extension to another schema. To deal with that without restricting things unnecessarily, add a "no_relocate" option to extension control files. This allows an extension to specify that it cannot handle relocation of some of its required extensions, even if in themselves those extensions are relocatable. We detect "no_relocate" requests of dependent extensions during ALTER EXTENSION SET SCHEMA. Regina Obe, reviewed by Sandro Santilli and myself Discussion: https://postgr.es/m/003001d8f4ae$402282c0$c0678840$@pcorp.us
1 parent 30e9f26 commit 72a5b1f

12 files changed

+272
-8
lines changed

doc/src/sgml/extend.sgml

+42-3
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,21 @@ RETURNS anycompatible AS ...
739739
</listitem>
740740
</varlistentry>
741741

742+
<varlistentry id="extend-extensions-files-no-relocate">
743+
<term><varname>no_relocate</varname> (<type>string</type>)</term>
744+
<listitem>
745+
<para>
746+
A list of names of extensions that this extension depends on that
747+
should be barred from changing their schemas via <command>ALTER
748+
EXTENSION ... SET SCHEMA</command>.
749+
This is needed if this extension's script references the name
750+
of a required extension's schema (using
751+
the <literal>@extschema:<replaceable>name</replaceable>@</literal>
752+
syntax) in a way that cannot track renames.
753+
</para>
754+
</listitem>
755+
</varlistentry>
756+
742757
<varlistentry id="extend-extensions-files-superuser">
743758
<term><varname>superuser</varname> (<type>boolean</type>)</term>
744759
<listitem>
@@ -902,8 +917,9 @@ RETURNS anycompatible AS ...
902917
For such an extension, set <literal>relocatable = false</literal> in its
903918
control file, and use <literal>@extschema@</literal> to refer to the target
904919
schema in the script file. All occurrences of this string will be
905-
replaced by the actual target schema's name before the script is
906-
executed. The user can set the target schema using the
920+
replaced by the actual target schema's name (double-quoted if
921+
necessary) before the script is executed. The user can set the
922+
target schema using the
907923
<literal>SCHEMA</literal> option of <command>CREATE EXTENSION</command>.
908924
</para>
909925
</listitem>
@@ -916,7 +932,7 @@ RETURNS anycompatible AS ...
916932
will prevent use of the <literal>SCHEMA</literal> option of <command>CREATE
917933
EXTENSION</command>, unless it specifies the same schema named in the control
918934
file. This choice is typically necessary if the extension contains
919-
internal assumptions about schema names that can't be replaced by
935+
internal assumptions about its schema name that can't be replaced by
920936
uses of <literal>@extschema@</literal>. The <literal>@extschema@</literal>
921937
substitution mechanism is available in this case too, although it is
922938
of limited use since the schema name is determined by the control file.
@@ -969,6 +985,29 @@ SET LOCAL search_path TO @extschema@, pg_temp;
969985
setting of <varname>search_path</varname> during creation of dependent
970986
extensions.
971987
</para>
988+
989+
<para>
990+
If an extension references objects belonging to another extension,
991+
it is recommended to schema-qualify those references. To do that,
992+
write <literal>@extschema:<replaceable>name</replaceable>@</literal>
993+
in the extension's script file, where <replaceable>name</replaceable>
994+
is the name of the other extension (which must be listed in this
995+
extension's <literal>requires</literal> list). This string will be
996+
replaced by the name (double-quoted if necessary) of that extension's
997+
target schema.
998+
Although this notation avoids the need to make hard-wired assumptions
999+
about schema names in the extension's script file, its use may embed
1000+
the other extension's schema name into the installed objects of this
1001+
extension. (Typically, that happens
1002+
when <literal>@extschema:<replaceable>name</replaceable>@</literal> is
1003+
used inside a string literal, such as a function body or
1004+
a <varname>search_path</varname> setting. In other cases, the object
1005+
reference is reduced to an OID during parsing and does not require
1006+
subsequent lookups.) If the other extension's schema name is so
1007+
embedded, you should prevent the other extension from being relocated
1008+
after yours is installed, by adding the name of the other extension to
1009+
this one's <literal>no_relocate</literal> list.
1010+
</para>
9721011
</sect2>
9731012

9741013
<sect2 id="extend-extensions-config-tables">

src/backend/commands/extension.c

+78-3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ typedef struct ExtensionControlFile
9090
bool trusted; /* allow becoming superuser on the fly? */
9191
int encoding; /* encoding of the script file, or -1 */
9292
List *requires; /* names of prerequisite extensions */
93+
List *no_relocate; /* names of prerequisite extensions that
94+
* should not be relocated */
9395
} ExtensionControlFile;
9496

9597
/*
@@ -606,6 +608,21 @@ parse_extension_control_file(ExtensionControlFile *control,
606608
item->name)));
607609
}
608610
}
611+
else if (strcmp(item->name, "no_relocate") == 0)
612+
{
613+
/* Need a modifiable copy of string */
614+
char *rawnames = pstrdup(item->value);
615+
616+
/* Parse string into list of identifiers */
617+
if (!SplitIdentifierString(rawnames, ',', &control->no_relocate))
618+
{
619+
/* syntax error in name list */
620+
ereport(ERROR,
621+
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
622+
errmsg("parameter \"%s\" must be a list of extension names",
623+
item->name)));
624+
}
625+
}
609626
else
610627
ereport(ERROR,
611628
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -845,6 +862,8 @@ extension_is_trusted(ExtensionControlFile *control)
845862
* Execute the appropriate script file for installing or updating the extension
846863
*
847864
* If from_version isn't NULL, it's an update
865+
*
866+
* Note: requiredSchemas must be one-for-one with the control->requires list
848867
*/
849868
static void
850869
execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
@@ -860,6 +879,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
860879
int save_nestlevel;
861880
StringInfoData pathbuf;
862881
ListCell *lc;
882+
ListCell *lc2;
863883

864884
/*
865885
* Enforce superuser-ness if appropriate. We postpone these checks until
@@ -1030,6 +1050,27 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10301050
CStringGetTextDatum(qSchemaName));
10311051
}
10321052

1053+
/*
1054+
* Likewise, substitute required extensions' schema names for
1055+
* occurrences of @extschema:extension_name@.
1056+
*/
1057+
Assert(list_length(control->requires) == list_length(requiredSchemas));
1058+
forboth(lc, control->requires, lc2, requiredSchemas)
1059+
{
1060+
char *reqextname = (char *) lfirst(lc);
1061+
Oid reqschema = lfirst_oid(lc2);
1062+
char *schemaName = get_namespace_name(reqschema);
1063+
const char *qSchemaName = quote_identifier(schemaName);
1064+
char *repltoken;
1065+
1066+
repltoken = psprintf("@extschema:%s@", reqextname);
1067+
t_sql = DirectFunctionCall3Coll(replace_text,
1068+
C_COLLATION_OID,
1069+
t_sql,
1070+
CStringGetTextDatum(repltoken),
1071+
CStringGetTextDatum(qSchemaName));
1072+
}
1073+
10331074
/*
10341075
* If module_pathname was set in the control file, substitute its
10351076
* value for occurrences of MODULE_PATHNAME.
@@ -2817,9 +2858,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
28172858
Oid dep_oldNspOid;
28182859

28192860
/*
2820-
* Ignore non-membership dependencies. (Currently, the only other
2821-
* case we could see here is a normal dependency from another
2822-
* extension.)
2861+
* If a dependent extension has a no_relocate request for this
2862+
* extension, disallow SET SCHEMA. (XXX it's a bit ugly to do this in
2863+
* the same loop that's actually executing the renames: we may detect
2864+
* the error condition only after having expended a fair amount of
2865+
* work. However, the alternative is to do two scans of pg_depend,
2866+
* which seems like optimizing for failure cases. The rename work
2867+
* will all roll back cleanly enough if we do fail here.)
2868+
*/
2869+
if (pg_depend->deptype == DEPENDENCY_NORMAL &&
2870+
pg_depend->classid == ExtensionRelationId)
2871+
{
2872+
char *depextname = get_extension_name(pg_depend->objid);
2873+
ExtensionControlFile *dcontrol;
2874+
ListCell *lc;
2875+
2876+
dcontrol = read_extension_control_file(depextname);
2877+
foreach(lc, dcontrol->no_relocate)
2878+
{
2879+
char *nrextname = (char *) lfirst(lc);
2880+
2881+
if (strcmp(nrextname, NameStr(extForm->extname)) == 0)
2882+
{
2883+
ereport(ERROR,
2884+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2885+
errmsg("cannot SET SCHEMA of extension \"%s\" because other extensions prevent it",
2886+
NameStr(extForm->extname)),
2887+
errdetail("Extension \"%s\" requests no relocation of extension \"%s\".",
2888+
depextname,
2889+
NameStr(extForm->extname))));
2890+
}
2891+
}
2892+
}
2893+
2894+
/*
2895+
* Otherwise, ignore non-membership dependencies. (Currently, the
2896+
* only other case we could see here is a normal dependency from
2897+
* another extension.)
28232898
*/
28242899
if (pg_depend->deptype != DEPENDENCY_EXTENSION)
28252900
continue;

src/test/modules/test_extensions/Makefile

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support"
66
EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
77
test_ext7 test_ext8 test_ext_cine test_ext_cor \
88
test_ext_cyclic1 test_ext_cyclic2 \
9-
test_ext_evttrig
9+
test_ext_evttrig \
10+
test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
11+
1012
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
1113
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
1214
test_ext7--1.0.sql test_ext7--1.0--2.0.sql test_ext8--1.0.sql \
1315
test_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \
1416
test_ext_cor--1.0.sql \
1517
test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \
16-
test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql
18+
test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \
19+
test_ext_req_schema1--1.0.sql \
20+
test_ext_req_schema2--1.0.sql \
21+
test_ext_req_schema3--1.0.sql
1722

1823
REGRESS = test_extensions test_extdepend
1924

src/test/modules/test_extensions/expected/test_extensions.out

+77
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,80 @@ Objects in extension "test_ext_cine"
312312
table ext_cine_tab3
313313
(9 rows)
314314

315+
--
316+
-- Test @extschema:extname@ syntax and no_relocate option
317+
--
318+
CREATE SCHEMA test_s_dep;
319+
CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
320+
CREATE EXTENSION test_ext_req_schema3 CASCADE;
321+
NOTICE: installing required extension "test_ext_req_schema2"
322+
SELECT test_s_dep.dep_req1();
323+
dep_req1
324+
----------
325+
req1
326+
(1 row)
327+
328+
SELECT dep_req2();
329+
dep_req2
330+
-----------
331+
req1 req2
332+
(1 row)
333+
334+
SELECT dep_req3();
335+
dep_req3
336+
-----------
337+
req1 req3
338+
(1 row)
339+
340+
SELECT dep_req3b();
341+
dep_req3b
342+
-----------------
343+
req1 req2 req3b
344+
(1 row)
345+
346+
CREATE SCHEMA test_s_dep2;
347+
ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- fails
348+
ERROR: cannot SET SCHEMA of extension "test_ext_req_schema1" because other extensions prevent it
349+
DETAIL: Extension "test_ext_req_schema3" requests no relocation of extension "test_ext_req_schema1".
350+
ALTER EXTENSION test_ext_req_schema2 SET SCHEMA test_s_dep; -- allowed
351+
SELECT test_s_dep.dep_req1();
352+
dep_req1
353+
----------
354+
req1
355+
(1 row)
356+
357+
SELECT test_s_dep.dep_req2();
358+
dep_req2
359+
-----------
360+
req1 req2
361+
(1 row)
362+
363+
SELECT dep_req3();
364+
dep_req3
365+
-----------
366+
req1 req3
367+
(1 row)
368+
369+
SELECT dep_req3b(); -- fails
370+
ERROR: function public.dep_req2() does not exist
371+
LINE 1: SELECT public.dep_req2() || ' req3b'
372+
^
373+
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
374+
QUERY: SELECT public.dep_req2() || ' req3b'
375+
CONTEXT: SQL function "dep_req3b" during startup
376+
DROP EXTENSION test_ext_req_schema3;
377+
ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
378+
SELECT test_s_dep2.dep_req1();
379+
dep_req1
380+
----------
381+
req1
382+
(1 row)
383+
384+
SELECT test_s_dep.dep_req2();
385+
dep_req2
386+
-----------
387+
req1 req2
388+
(1 row)
389+
390+
DROP EXTENSION test_ext_req_schema1 CASCADE;
391+
NOTICE: drop cascades to extension test_ext_req_schema2

src/test/modules/test_extensions/meson.build

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ test_install_data += files(
3030
'test_ext_evttrig--1.0--2.0.sql',
3131
'test_ext_evttrig--1.0.sql',
3232
'test_ext_evttrig.control',
33+
'test_ext_req_schema1--1.0.sql',
34+
'test_ext_req_schema1.control',
35+
'test_ext_req_schema2--1.0.sql',
36+
'test_ext_req_schema2.control',
37+
'test_ext_req_schema3--1.0.sql',
38+
'test_ext_req_schema3.control',
3339
)
3440

3541
tests += {

src/test/modules/test_extensions/sql/test_extensions.sql

+23
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,26 @@ CREATE EXTENSION test_ext_cine;
209209
ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
210210

211211
\dx+ test_ext_cine
212+
213+
--
214+
-- Test @extschema:extname@ syntax and no_relocate option
215+
--
216+
CREATE SCHEMA test_s_dep;
217+
CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
218+
CREATE EXTENSION test_ext_req_schema3 CASCADE;
219+
SELECT test_s_dep.dep_req1();
220+
SELECT dep_req2();
221+
SELECT dep_req3();
222+
SELECT dep_req3b();
223+
CREATE SCHEMA test_s_dep2;
224+
ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- fails
225+
ALTER EXTENSION test_ext_req_schema2 SET SCHEMA test_s_dep; -- allowed
226+
SELECT test_s_dep.dep_req1();
227+
SELECT test_s_dep.dep_req2();
228+
SELECT dep_req3();
229+
SELECT dep_req3b(); -- fails
230+
DROP EXTENSION test_ext_req_schema3;
231+
ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
232+
SELECT test_s_dep2.dep_req1();
233+
SELECT test_s_dep.dep_req2();
234+
DROP EXTENSION test_ext_req_schema1 CASCADE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* src/test/modules/test_extensions/test_ext_req_schema1--1.0.sql */
2+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
3+
\echo Use "CREATE EXTENSION test_ext_req_schema1" to load this file. \quit
4+
5+
CREATE FUNCTION dep_req1() RETURNS text
6+
LANGUAGE SQL AS $$ SELECT 'req1' $$;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
comment = 'Required extension to be referenced'
2+
default_version = '1.0'
3+
relocatable = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* src/test/modules/test_extensions/test_ext_req_schema2--1.0.sql */
2+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
3+
\echo Use "CREATE EXTENSION test_ext_req_schema2" to load this file. \quit
4+
5+
-- This formulation can handle relocation of the required extension.
6+
CREATE FUNCTION dep_req2() RETURNS text
7+
BEGIN ATOMIC
8+
SELECT @extschema:test_ext_req_schema1@.dep_req1() || ' req2';
9+
END;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
comment = 'Test schema referencing of required extensions'
2+
default_version = '1.0'
3+
relocatable = true
4+
requires = 'test_ext_req_schema1'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* src/test/modules/test_extensions/test_ext_req_schema3--1.0.sql */
2+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
3+
\echo Use "CREATE EXTENSION test_ext_req_schema3" to load this file. \quit
4+
5+
-- This formulation cannot handle relocation of the required extension.
6+
CREATE FUNCTION dep_req3() RETURNS text
7+
LANGUAGE SQL IMMUTABLE PARALLEL SAFE
8+
AS $$ SELECT @extschema:test_ext_req_schema1@.dep_req1() || ' req3' $$;
9+
10+
CREATE FUNCTION dep_req3b() RETURNS text
11+
LANGUAGE SQL IMMUTABLE PARALLEL SAFE
12+
AS $$ SELECT @extschema:test_ext_req_schema2@.dep_req2() || ' req3b' $$;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
comment = 'Test schema referencing of 2 required extensions'
2+
default_version = '1.0'
3+
relocatable = true
4+
requires = 'test_ext_req_schema1, test_ext_req_schema2'
5+
no_relocate = 'test_ext_req_schema1'

0 commit comments

Comments
 (0)