From 8e067aada04bcca5e174d3b34637605bdb2c7665 Mon Sep 17 00:00:00 2001 From: Jim Mlodgenski Date: Mon, 22 Jun 2026 16:49:59 -0400 Subject: [PATCH] Add pgtle.set_extension_schema() to set an extension's schema Schema support added in pg_tle 1.5 stores the target schema in an extension's control function, but there was no way to set or change it afterward. An extension installed before 1.5, or with the wrong schema, could only adopt one by uninstalling and reinstalling, which drops any data in its tables. Add pgtle.set_extension_schema(name, schema), which rewrites just the control function in place. It takes the schema as text (matching install_extension's schema argument, which likewise only records the name) and accepts NULL to clear it; an empty string is rejected. The change affects future CREATE EXTENSION calls; an already-created extension is left untouched. Fixes #301. --- Makefile | 5 +- docs/03_managing_extensions.md | 27 ++++ include/tleextension.h | 8 ++ pg_tle--1.5.2--1.5.3.sql | 40 ++++++ src/tleextension.c | 160 ++++++++++++++++++++-- test/expected/pg_tle_extension_schema.out | 118 +++++++++++++++- test/sql/pg_tle_extension_schema.sql | 67 ++++++++- 7 files changed, 405 insertions(+), 20 deletions(-) create mode 100644 pg_tle--1.5.2--1.5.3.sql diff --git a/Makefile b/Makefile index 8db0333e..beabefee 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ EXTENSION = pg_tle -EXTVERSION = 1.5.2 +EXTVERSION = 1.5.3 SCHEMA = pgtle MODULE_big = $(EXTENSION) @@ -9,7 +9,8 @@ OBJS = src/tleextension.o src/guc-file.o src/feature.o src/passcheck.o src/uni_a EXTRA_CLEAN = src/guc-file.c pg_tle.control pg_tle--$(EXTVERSION).sql DATA = pg_tle.control pg_tle--1.0.0.sql pg_tle--1.0.0--1.0.1.sql pg_tle--1.0.1--1.0.4.sql pg_tle--1.0.4.sql pg_tle--1.0.4--1.1.1.sql \ pg_tle--1.1.0--1.1.1.sql pg_tle--1.1.1.sql pg_tle--1.1.1--1.2.0.sql pg_tle--1.2.0--1.3.0.sql pg_tle--1.3.0--1.3.3.sql \ - pg_tle--1.3.3--1.3.4.sql pg_tle--1.3.4--1.4.0.sql pg_tle--1.4.0--1.5.0.sql pg_tle--1.5.0--1.5.2.sql + pg_tle--1.3.3--1.3.4.sql pg_tle--1.3.4--1.4.0.sql pg_tle--1.4.0--1.5.0.sql pg_tle--1.5.0--1.5.2.sql \ + pg_tle--1.5.2--1.5.3.sql TESTS = $(wildcard test/sql/*.sql) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) diff --git a/docs/03_managing_extensions.md b/docs/03_managing_extensions.md index c6b678b9..2458747b 100644 --- a/docs/03_managing_extensions.md +++ b/docs/03_managing_extensions.md @@ -278,6 +278,33 @@ This functions returns `true` on success. * `name`: The name of the extension. This is the value used when calling `CREATE EXTENSION`. * `version`: The version of the extension to set the default. +### `pgtle.set_extension_schema(name text, schema text)` + +`set_extension_schema` sets, changes, or clears the schema recorded for an installed extension. The schema is the value that PostgreSQL uses to place the extension's objects when `CREATE EXTENSION` is called. This is helpful for pinning an extension that was installed without a schema, for example before schema support was added in `pg_tle` 1.5.0, to a specific schema without uninstalling and reinstalling it (which would drop any data in its tables). + +Pass `NULL` as `schema` to clear the recorded schema, reverting the extension to having no fixed schema. + +This only affects future `CREATE EXTENSION` calls (including restoring from a `pg_dump`). It does not move an extension that is already created; use `ALTER EXTENSION ... SET SCHEMA` for that. + +If the extension in `name` does not already exist, this returns an error. + +This function returns `true` on success. + +#### Role + +`pgtle_admin` + +#### Arguments + +* `name`: The name of the extension. This is the value used when calling `CREATE EXTENSION`. +* `schema`: The schema in which the extension's objects are created, or `NULL` to clear the recorded schema. + +#### Example + +```sql +SELECT pgtle.set_extension_schema('pg_tle_test', 'tle_schema'); +``` + ### `pgtle.uninstall_extension(extname text)` `uninstall_extension` removes all versions of an extension from a database. This prevents future calls of `CREATE EXTENSION` from installing the extension. If the extension does not exist in the database, then an error is raised. diff --git a/include/tleextension.h b/include/tleextension.h index 58583afa..5d0234ee 100644 --- a/include/tleextension.h +++ b/include/tleextension.h @@ -25,6 +25,14 @@ #define PG_TLE_INNER_STR "$_pgtle_i_$" #define PG_TLE_ADMIN "pgtle_admin" +/* + * Characters that cannot be quoted consistently both inside and outside of + * string literals. Identifiers substituted into extension scripts, or stored + * where they will be substituted later, are rejected if they contain any of + * these. + */ +#define PG_TLE_QUOTING_RELEVANT_CHARS "\"$'\\" + /* * creating_extension is only true while running a CREATE EXTENSION or ALTER * EXTENSION UPDATE command. It instructs recordDependencyOnCurrentExtension() diff --git a/pg_tle--1.5.2--1.5.3.sql b/pg_tle--1.5.2--1.5.3.sql new file mode 100644 index 00000000..c050638d --- /dev/null +++ b/pg_tle--1.5.2--1.5.3.sql @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION pg_tle" to load this file. \quit + +CREATE FUNCTION pgtle.set_extension_schema +( + name text, + schema text +) +RETURNS boolean +SET search_path TO 'pgtle' +AS 'MODULE_PATHNAME', 'pg_tle_set_extension_schema' +LANGUAGE C; + +REVOKE EXECUTE ON FUNCTION pgtle.set_extension_schema +( + name text, + schema text +) FROM PUBLIC; + +GRANT EXECUTE ON FUNCTION pgtle.set_extension_schema +( + name text, + schema text +) TO pgtle_admin; diff --git a/src/tleextension.c b/src/tleextension.c index 91b92d19..0b7c2e21 100644 --- a/src/tleextension.c +++ b/src/tleextension.c @@ -1411,16 +1411,6 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, char *c_sql = read_extension_script_file(control, filename); Datum t_sql; - /* - * We filter each substitution through quote_identifier(). When the - * arg contains one of the following characters, no one collection of - * quoting can work inside $$dollar-quoted string literals$$, - * 'single-quoted string literals', and outside of any literal. To - * avoid a security snare for extension authors, error on substitution - * for arguments containing these. - */ - const char *quoting_relevant_chars = "\"$'\\"; - /* We use various functions that want to operate on text datums */ t_sql = CStringGetTextDatum(c_sql); @@ -1450,11 +1440,20 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, t_sql, CStringGetTextDatum("@extowner@"), CStringGetTextDatum(qUserName)); - if (strpbrk(userName, quoting_relevant_chars)) + + /* + * We filter each substitution through quote_identifier(). When + * the arg contains one of these characters, no one collection of + * quoting can work inside $$dollar-quoted string literals$$, + * 'single-quoted string literals', and outside of any literal. To + * avoid a security snare for extension authors, error on + * substitution for arguments containing these. + */ + if (strpbrk(userName, PG_TLE_QUOTING_RELEVANT_CHARS)) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid character in extension owner: must not contain any of \"%s\"", - quoting_relevant_chars))); + PG_TLE_QUOTING_RELEVANT_CHARS))); } /* @@ -1474,11 +1473,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, t_sql, CStringGetTextDatum("@extschema@"), CStringGetTextDatum(qSchemaName)); - if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars)) + if (t_sql != old && strpbrk(schemaName, PG_TLE_QUOTING_RELEVANT_CHARS)) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"", - control->name, quoting_relevant_chars))); + control->name, PG_TLE_QUOTING_RELEVANT_CHARS))); } /* @@ -5229,6 +5228,139 @@ pg_tle_set_default_version(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } +Datum pg_tle_set_extension_schema(PG_FUNCTION_ARGS); + +/* + * Set (or clear) the target schema recorded in an extension's control + * function. This rewrites just the control function in place, so future + * CREATE EXTENSION (or a fresh database restored from pg_dump) honors the new + * schema while an already-created extension is left untouched. A NULL schema + * clears it, leaving the extension with no fixed schema. + */ +PG_FUNCTION_INFO_V1(pg_tle_set_extension_schema); +Datum +pg_tle_set_extension_schema(PG_FUNCTION_ARGS) +{ + int spi_rc; + char *extname; + char *schemaName = NULL; + char *ctlname; + Oid ctlfuncid; + StringInfo ctlstr; + char *ctlsql; + ExtensionControlFile *control; + char *filename; + + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("\"name\" is a required argument."))); + + extname = text_to_cstring(PG_GETARG_TEXT_PP(0)); + check_valid_extension_name(extname); + + /* + * Verify that extname does not already exist as a standard file-based + * extension. + */ + filename = get_extension_control_filename(extname); + if (filestat(filename)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("control file already exists for the %s extension", extname))); + + /* + * A NULL schema clears the recorded schema; otherwise validate it. The + * schema name is embedded verbatim into the control function and later + * substituted for @extschema@, so reject characters that cannot be quoted + * consistently both inside and outside of string literals. + */ + if (!PG_ARGISNULL(1)) + { + schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1)); + + /* + * Reject the empty string rather than record schema = '', which would + * pin the extension to an unusable zero-length schema. Use a NULL + * argument to clear the schema instead. + */ + if (schemaName[0] == '\0') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("extension schema must not be empty"), + errhint("Pass NULL as the schema to clear it."))); + + if (strpbrk(schemaName, PG_TLE_QUOTING_RELEVANT_CHARS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid character in extension schema: must not contain any of \"%s\"", + PG_TLE_QUOTING_RELEVANT_CHARS))); + } + + /* + * Verify that the extension exists as a TLE extension by looking up its + * control function. + */ + ctlname = psprintf("%s.control", extname); + ctlfuncid = get_tlefunc_oid_if_exists(ctlname); + if (ctlfuncid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("extension \"%s\" does not exist", extname), + errhint("Try installing the extension with \"%s.install_extension\".", PG_TLE_NSPNAME))); + + /* + * Load the current control state, then replace the schema. + */ + control = build_default_extension_control_file(extname); + + SET_TLEEXT; + parse_extension_control_file(control, NULL); + UNSET_TLEEXT; + + control->schema = schemaName; + + ctlstr = build_extension_control_file_string(control); + + /* + * Validate that there are no injections using the dollar-quoted strings + */ + if (!(validate_tle_sql(ctlstr->data))) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid character in extension definition"), + errdetail("Use of string delimiters %s and %s are forbidden in extension definitions.", + PG_TLE_OUTER_STR, PG_TLE_INNER_STR))); + + ctlsql = psprintf( + "CREATE OR REPLACE FUNCTION %s.%s() RETURNS TEXT AS %s" + "SELECT %s%s%s%s LANGUAGE SQL", + quote_identifier(PG_TLE_NSPNAME), quote_identifier(ctlname), + PG_TLE_OUTER_STR, PG_TLE_INNER_STR, + ctlstr->data, + PG_TLE_INNER_STR, PG_TLE_OUTER_STR); + + /* flag that we are manipulating pg_tle artifacts */ + SET_TLEART; + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + spi_rc = SPI_exec(ctlsql, 0); + if (spi_rc != SPI_OK_UTILITY) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("failed to update schema for \"%s\"", extname))); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + /* flag that we are done manipulating pg_tle artifacts */ + UNSET_TLEART; + + PG_RETURN_BOOL(true); +} + /* * Convert text array to list of strings. * diff --git a/test/expected/pg_tle_extension_schema.out b/test/expected/pg_tle_extension_schema.out index 06a013d0..d8723844 100644 --- a/test/expected/pg_tle_extension_schema.out +++ b/test/expected/pg_tle_extension_schema.out @@ -16,6 +16,9 @@ * * 4. pgtle.available_extensions() and pgtle.available_extension_versions() * print the correct output for a variety of extensions. + * + * 5. pgtle.set_extension_schema() sets, changes, or clears the schema recorded + * for an already-installed extension. */ \pset pager off /* @@ -280,7 +283,118 @@ DROP FUNCTION pgtle."my_tle_2.control" CASCADE; DROP FUNCTION pgtle."my_tle_1--1.0.sql" CASCADE; NOTICE: drop cascades to extension my_tle_1 DROP FUNCTION pgtle."my_tle_1.control" CASCADE; -DROP EXTENSION pg_tle CASCADE; -DROP SCHEMA pgtle; +/* + * 5. pgtle.set_extension_schema() sets, changes, or clears the schema recorded + * for an already-installed extension. This lets an extension installed + * without a schema (e.g. before pg_tle 1.5) adopt one without being + * uninstalled and reinstalled. + */ +-- set_extension_schema was added in pg_tle 1.5.3; the earlier sections leave +-- pg_tle at 1.5.0, so update to the latest version first. +ALTER EXTENSION pg_tle UPDATE; +-- Reset the schemas used here (earlier sections may have left them behind). +DROP SCHEMA IF EXISTS my_tle_schema_1 CASCADE; +DROP SCHEMA IF EXISTS my_tle_schema_2 CASCADE; +CREATE SCHEMA my_tle_schema_1; +CREATE SCHEMA my_tle_schema_2; +-- Install an extension without a schema, as pg_tle did before 1.5. +SELECT pgtle.install_extension('my_tle', '1.0', 'My TLE', + $_pgtle_$ + CREATE OR REPLACE FUNCTION my_tle_func() RETURNS INT LANGUAGE SQL AS + 'SELECT 1'; + $_pgtle_$); + install_extension +------------------- + t +(1 row) + +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + name | schema +--------+-------- + my_tle | +(1 row) + +-- Adopt a schema after the fact. +SELECT pgtle.set_extension_schema('my_tle', 'my_tle_schema_1'); + set_extension_schema +---------------------- + t +(1 row) + +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + name | schema +--------+----------------- + my_tle | my_tle_schema_1 +(1 row) + +-- A newly created extension honors the adopted schema and is pinned to it. +CREATE EXTENSION my_tle; +SELECT n.nspname FROM pg_extension e + INNER JOIN pg_namespace n ON e.extnamespace = n.oid + WHERE e.extname = 'my_tle'; + nspname +----------------- + my_tle_schema_1 +(1 row) + +SELECT my_tle_schema_1.my_tle_func(); + my_tle_func +------------- + 1 +(1 row) + +DROP EXTENSION my_tle; +-- Cannot be created in a different schema. +CREATE EXTENSION my_tle SCHEMA my_tle_schema_2; +ERROR: extension "my_tle" must be installed in schema "my_tle_schema_1" +-- Changing the schema is allowed. +SELECT pgtle.set_extension_schema('my_tle', 'my_tle_schema_2'); + set_extension_schema +---------------------- + t +(1 row) + +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + name | schema +--------+----------------- + my_tle | my_tle_schema_2 +(1 row) + +-- Passing NULL clears the recorded schema. +SELECT pgtle.set_extension_schema('my_tle', NULL); + set_extension_schema +---------------------- + t +(1 row) + +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + name | schema +--------+-------- + my_tle | +(1 row) + +-- Negative cases. +-- The extension must exist. +SELECT pgtle.set_extension_schema('does_not_exist', 'my_tle_schema_1'); +ERROR: extension "does_not_exist" does not exist +HINT: Try installing the extension with "pgtle.install_extension". +-- The schema name must not contain characters that cannot be quoted safely. +SELECT pgtle.set_extension_schema('my_tle', 'bad"schema'); +ERROR: invalid character in extension schema: must not contain any of ""$'\" +-- The empty string is rejected; NULL is the way to clear the schema. +SELECT pgtle.set_extension_schema('my_tle', ''); +ERROR: extension schema must not be empty +HINT: Pass NULL as the schema to clear it. +-- "name" is required. +SELECT pgtle.set_extension_schema(NULL, 'my_tle_schema_1'); +ERROR: "name" is a required argument. +SELECT pgtle.uninstall_extension('my_tle'); + uninstall_extension +--------------------- + t +(1 row) + DROP SCHEMA my_tle_schema_1; DROP SCHEMA my_tle_schema_2; +DROP EXTENSION pg_tle CASCADE; +DROP SCHEMA pgtle; diff --git a/test/sql/pg_tle_extension_schema.sql b/test/sql/pg_tle_extension_schema.sql index 1697a706..78e001cd 100644 --- a/test/sql/pg_tle_extension_schema.sql +++ b/test/sql/pg_tle_extension_schema.sql @@ -17,6 +17,9 @@ * * 4. pgtle.available_extensions() and pgtle.available_extension_versions() * print the correct output for a variety of extensions. + * + * 5. pgtle.set_extension_schema() sets, changes, or clears the schema recorded + * for an already-installed extension. */ \pset pager off @@ -172,7 +175,67 @@ DROP FUNCTION pgtle."my_tle_2--1.0.sql" CASCADE; DROP FUNCTION pgtle."my_tle_2.control" CASCADE; DROP FUNCTION pgtle."my_tle_1--1.0.sql" CASCADE; DROP FUNCTION pgtle."my_tle_1.control" CASCADE; -DROP EXTENSION pg_tle CASCADE; -DROP SCHEMA pgtle; + +/* + * 5. pgtle.set_extension_schema() sets, changes, or clears the schema recorded + * for an already-installed extension. This lets an extension installed + * without a schema (e.g. before pg_tle 1.5) adopt one without being + * uninstalled and reinstalled. + */ + +-- set_extension_schema was added in pg_tle 1.5.3; the earlier sections leave +-- pg_tle at 1.5.0, so update to the latest version first. +ALTER EXTENSION pg_tle UPDATE; + +-- Reset the schemas used here (earlier sections may have left them behind). +DROP SCHEMA IF EXISTS my_tle_schema_1 CASCADE; +DROP SCHEMA IF EXISTS my_tle_schema_2 CASCADE; +CREATE SCHEMA my_tle_schema_1; +CREATE SCHEMA my_tle_schema_2; + +-- Install an extension without a schema, as pg_tle did before 1.5. +SELECT pgtle.install_extension('my_tle', '1.0', 'My TLE', + $_pgtle_$ + CREATE OR REPLACE FUNCTION my_tle_func() RETURNS INT LANGUAGE SQL AS + 'SELECT 1'; + $_pgtle_$); +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + +-- Adopt a schema after the fact. +SELECT pgtle.set_extension_schema('my_tle', 'my_tle_schema_1'); +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + +-- A newly created extension honors the adopted schema and is pinned to it. +CREATE EXTENSION my_tle; +SELECT n.nspname FROM pg_extension e + INNER JOIN pg_namespace n ON e.extnamespace = n.oid + WHERE e.extname = 'my_tle'; +SELECT my_tle_schema_1.my_tle_func(); +DROP EXTENSION my_tle; +-- Cannot be created in a different schema. +CREATE EXTENSION my_tle SCHEMA my_tle_schema_2; + +-- Changing the schema is allowed. +SELECT pgtle.set_extension_schema('my_tle', 'my_tle_schema_2'); +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + +-- Passing NULL clears the recorded schema. +SELECT pgtle.set_extension_schema('my_tle', NULL); +SELECT name, schema FROM pgtle.available_extensions() WHERE name = 'my_tle'; + +-- Negative cases. +-- The extension must exist. +SELECT pgtle.set_extension_schema('does_not_exist', 'my_tle_schema_1'); +-- The schema name must not contain characters that cannot be quoted safely. +SELECT pgtle.set_extension_schema('my_tle', 'bad"schema'); +-- The empty string is rejected; NULL is the way to clear the schema. +SELECT pgtle.set_extension_schema('my_tle', ''); +-- "name" is required. +SELECT pgtle.set_extension_schema(NULL, 'my_tle_schema_1'); + +SELECT pgtle.uninstall_extension('my_tle'); DROP SCHEMA my_tle_schema_1; DROP SCHEMA my_tle_schema_2; + +DROP EXTENSION pg_tle CASCADE; +DROP SCHEMA pgtle;