diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 64f8e133caeb..f42c096ad3a5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
+
+ owned_schema (boolean)
+
+
+ An extension should set owned_schema to
+ true in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ CREATE EXTENSION. The default is
+ false, i.e., the extension can be installed into an
+ existing schema.
+
+
+ Having a schema owned by the extension can make it much easier to
+ reason about possible search_path injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ search_path of a SECURITY DEFINER
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus cause malicious to be executed
+ as superuser. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's
+ recommended to manually change the search_path.
+
+
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+
+
+
+
schema (string)
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c4944..9fd2c3429e81 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,9 @@ CREATE EXTENSION [ IF NOT EXISTS ] extension_name
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist, unless
+ owned_schema is set to true in
+ the control file, then the schema must not exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 180f4af9be36..c55278014ef2 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -90,6 +90,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -611,6 +613,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1744,8 +1754,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1767,7 +1780,10 @@ CreateExtensionInternal(char *extensionName,
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1788,8 +1804,22 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1856,6 +1886,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2061,7 +2092,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2091,6 +2123,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);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2135,6 +2168,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3053,11 +3097,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3066,22 +3109,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3105,14 +3132,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ ownedSchema = extForm->extownedschema;
+
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3185,6 +3236,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3228,18 +3286,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (ownedSchema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index d44f8c262baa..0ed7b4267fbe 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/origin.h"
@@ -184,12 +185,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -197,28 +200,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -233,10 +238,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 105e917aa7b9..ade369ffa2a8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1871,6 +1871,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5754,7 +5767,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5768,19 +5781,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -5937,6 +5964,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -5945,7 +5973,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -5961,6 +5996,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -5974,6 +6010,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11567,9 +11604,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11681,6 +11718,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index b426b5e47361..5a001ea2cee7 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 9214ebedafa3..022bd6dd92b9 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62beb71da288..eb3ce8873b22 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11743,7 +11743,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 24419bfb5c90..205c5c7245fe 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f3..a6594c19d7ee 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +26,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 72bae1bf254b..7bb91c645171 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -668,3 +668,53 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3c7e378bf359..c4913fb9c86b 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f1..a97866d00ea1 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 000000000000..672ab8e607f0
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 000000000000..531c38daefd6
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 000000000000..bfccaf4af829
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 000000000000..3cda1e123415
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true