diff --git a/Makefile b/Makefile index 8db0333..beabefe 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/README.md b/README.md index cb6a8bd..4a05470 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ There are examples for writing TLEs in several languages, including: ## Supported PostgreSQL versions -`pg_tle` 1.5.2 supports PostgreSQL major versions 12 to 18. +`pg_tle` 1.5.3 supports PostgreSQL major versions 12 to 18. `pg_tle` 1.5.0 supports PostgreSQL major versions 12 to 17. 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 0000000..1f256ad --- /dev/null +++ b/pg_tle--1.5.2--1.5.3.sql @@ -0,0 +1,150 @@ +/* + * 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 OR REPLACE FUNCTION pgtle.uninstall_extension(extname text) +RETURNS boolean +SET search_path TO 'pgtle' +AS $_pgtleie_$ + DECLARE + ctlname text; + sqlpattern text; + searchctl text; + searchsql text; + dropsql text; + pgtlensp text := 'pgtle'; + func text; + existsvar record; + BEGIN + + -- the control function is named exactly '.control' + ctlname := extname || '.control'; + /* + * Script functions are named '--.sql' and + * '----.sql'. Match them with a regex anchored on the + * '--' that follows the name: '^--.sql$'. '--' cannot + * occur in an extension name, so a prefix-named sibling like '_x' + * cannot match. regexp_replace() backslash-escapes any regex + * metacharacters in extname so it is matched literally. + */ + sqlpattern := '^' || regexp_replace(extname, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g') || '--.*\.sql$'; + searchctl := 'SELECT proname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE proname OPERATOR(pg_catalog.=) $1 AND n.nspname = $2'; + searchsql := 'SELECT proname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE proname OPERATOR(pg_catalog.~) $1 AND n.nspname = $2'; + + EXECUTE searchctl USING ctlname, pgtlensp INTO existsvar; + IF existsvar IS NULL THEN + RAISE EXCEPTION 'Extension % does not exist', extname USING ERRCODE = 'no_data_found'; + ELSE + FOR func IN EXECUTE searchctl USING ctlname, pgtlensp LOOP + dropsql := format('DROP FUNCTION %I()', func); + EXECUTE dropsql; + END LOOP; + END IF; + + EXECUTE searchsql USING sqlpattern, pgtlensp INTO existsvar; + IF existsvar IS NULL THEN + RAISE WARNING 'Extension % has an anomaly; control function exists, but no sql commands function exists', extname; + ELSE + FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP + dropsql := format('DROP FUNCTION %I()', func); + EXECUTE dropsql; + END LOOP; + END IF; + + RETURN true; + END; +$_pgtleie_$ +LANGUAGE plpgsql STRICT; + +-- uninstall an extension for a specific version +CREATE OR REPLACE FUNCTION pgtle.uninstall_extension(extname text, version text) +RETURNS boolean +SET search_path TO 'pgtle' +AS $_pgtleie_$ + DECLARE + ctlname text; + sqlpattern text; + countverssql text; + vers_count bigint; + defaultversql text; + defaultver text; + searchctl text; + searchsql text; + dropsql text; + pgtlensp text := 'pgtle'; + func_available_vers text := 'available_extension_versions()'; + func_available_ext text := 'available_extensions()'; + func text; + esc_ext text; + esc_ver text; + BEGIN + -- regex-escape name and version so any metacharacters are matched literally + esc_ext := regexp_replace(extname, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g'); + esc_ver := regexp_replace(version, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g'); + ctlname := extname || '.control'; + /* + * Match this version's install script ('--.sql') and every + * update-path script that uses the version as a token, via the alternation + * '( | .*-- | --.*)': + * -> '--.sql' (the version install script) + * .*-- -> '----.sql' (an update into this version) + * --.* -> '----.sql' (an update out of this version) + * '--' separates the tokens and cannot occur within a name or a version. + */ + sqlpattern := '^' || esc_ext || '--(' || esc_ver || '|.*--' || esc_ver || '|' || esc_ver || '--.*)\.sql$'; + countverssql := format('SELECT COUNT(*) FROM %s.%s WHERE name = $1', pgtlensp, func_available_vers); + defaultversql := format('SELECT default_version FROM %s.%s WHERE name = $1', pgtlensp, func_available_ext); + searchctl := 'SELECT proname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE proname OPERATOR(pg_catalog.=) $1 AND n.nspname = $2'; + searchsql := 'SELECT proname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE proname OPERATOR(pg_catalog.~) $1 AND n.nspname = $2'; + + EXECUTE countverssql USING extname INTO vers_count; + EXECUTE defaultversql USING extname INTO defaultver; + + IF vers_count > 1 THEN + -- if multiple versions exist and this is the default version, don't uninstall + IF version = defaultver THEN + RAISE EXCEPTION 'Can not uninstall default version of extension %, use set_default_version to update the default to another available version and retry', extname; + ELSE + -- remove the specified version sql file function only, don't remove control file function + FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP + dropsql := format('DROP FUNCTION %I()', func); + EXECUTE dropsql; + END LOOP; + END IF; + ELSE + -- check that the specified version matches the only version that exists + -- if it does then uninstall the extension completely + -- if it doesn't then don't uninstall anything to avoid accidental uninstall + IF version = defaultver THEN + FOR func IN EXECUTE searchctl USING ctlname, pgtlensp LOOP + dropsql := format('DROP FUNCTION %I()', func); + EXECUTE dropsql; + END LOOP; + FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP + dropsql := format('DROP FUNCTION %I()', func); + EXECUTE dropsql; + END LOOP; + ELSE + RAISE EXCEPTION 'Version % of extension % is not installed and therefore can not be uninstalled', extname, version; + END IF; + END IF; + + RETURN TRUE; + END; +$_pgtleie_$ +LANGUAGE plpgsql STRICT; diff --git a/test/expected/pg_tle_functions_acl.out b/test/expected/pg_tle_functions_acl.out index c3172fc..650e715 100644 --- a/test/expected/pg_tle_functions_acl.out +++ b/test/expected/pg_tle_functions_acl.out @@ -181,18 +181,17 @@ SELECT pgtle.set_default_version('', ''); ERROR: invalid extension name: "" DETAIL: Extension names must not be empty. SELECT pgtle.uninstall_extension(''); -ERROR: must be owner of function test_ext.control -CONTEXT: SQL statement "DROP FUNCTION "test_ext.control"()" -PL/pgSQL function uninstall_extension(text) line 22 at EXECUTE +ERROR: Extension does not exist +CONTEXT: PL/pgSQL function uninstall_extension(text) line 23 at RAISE SELECT pgtle.uninstall_extension('', ''); ERROR: Version of extension is not installed and therefore can not be uninstalled -CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 50 at RAISE +CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 61 at RAISE SELECT pgtle.uninstall_extension_if_exists(''); -ERROR: must be owner of function test_ext.control -CONTEXT: SQL statement "DROP FUNCTION "test_ext.control"()" -PL/pgSQL function uninstall_extension(text) line 22 at EXECUTE -SQL statement "SELECT pgtle.uninstall_extension(extname)" -PL/pgSQL function uninstall_extension_if_exists(text) line 3 at PERFORM + uninstall_extension_if_exists +------------------------------- + f +(1 row) + SELECT pgtle.uninstall_update_path('', '', ''); ERROR: Extension does not exist CONTEXT: PL/pgSQL function uninstall_update_path(text,text,text) line 16 at RAISE diff --git a/test/expected/pg_tle_management.out b/test/expected/pg_tle_management.out index b6dc0b3..1968e7c 100644 --- a/test/expected/pg_tle_management.out +++ b/test/expected/pg_tle_management.out @@ -586,7 +586,7 @@ SELECT pgtle.uninstall_extension('broken_ext'); -- error SELECT pgtle.uninstall_extension('bogus'); ERROR: Extension bogus does not exist -CONTEXT: PL/pgSQL function uninstall_extension(text) line 18 at RAISE +CONTEXT: PL/pgSQL function uninstall_extension(text) line 29 at RAISE -- uninstall_if_exists with a non-existent extension -- returns false, no error SELECT pgtle.uninstall_extension_if_exists('bogus'); @@ -716,7 +716,7 @@ SELECT pgtle.available_extension_versions(); -- fails SELECT pgtle.uninstall_extension('test42', '1.0'); ERROR: Can not uninstall default version of extension test42, use set_default_version to update the default to another available version and retry -CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 28 at RAISE +CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 44 at RAISE SELECT pgtle.available_extension_versions(); available_extension_versions --------------------------------------------------- @@ -742,7 +742,7 @@ SELECT pgtle.available_extension_versions(); -- fails SELECT pgtle.uninstall_extension('test42', '3.0'); ERROR: Version test42 of extension 3.0 is not installed and therefore can not be uninstalled -CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 50 at RAISE +CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 66 at RAISE SELECT pgtle.available_extension_versions(); available_extension_versions --------------------------------------------------- @@ -951,6 +951,106 @@ SELECT pgtle.uninstall_extension('n545678901234567890123456789012345678901234567 t (1 row) +-- uninstall must not remove another extension whose name it is a prefix of, nor treat '_' as a wildcard +-- set up a prefix target with an update path, a longer-named sibling, and an underscore decoy +SELECT pgtle.install_extension('col_a', '1.0', 'prefix', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension +------------------- + t +(1 row) + +SELECT pgtle.install_extension_version_sql('col_a', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension_version_sql +------------------------------- + t +(1 row) + +SELECT pgtle.install_update_path('col_a', '1.0', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$); + install_update_path +--------------------- + t +(1 row) + +SELECT pgtle.install_extension('col_a_b', '1.0', 'longer sibling', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension +------------------- + t +(1 row) + +SELECT pgtle.install_extension('col_axb', '1.0', 'underscore decoy', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension +------------------- + t +(1 row) + +-- prefix uninstall removes only col_a's artifacts; the sibling and decoy survive +SELECT pgtle.uninstall_extension('col_a'); + uninstall_extension +--------------------- + t +(1 row) + +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name; + name +--------- + col_a_b + col_axb +(2 rows) + +-- the '_' in col_a_b must not act as a wildcard and sweep up col_axb +SELECT pgtle.uninstall_extension('col_a_b'); + uninstall_extension +--------------------- + t +(1 row) + +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name; + name +--------- + col_axb +(1 row) + +-- the (name, version) form drops the control function by exact name, not by prefix, so pfx_b survives +SELECT pgtle.install_extension('pfx', '1.0', 'prefix2', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension +------------------- + t +(1 row) + +SELECT pgtle.install_extension('pfx_b', '1.0', 'sibling2', $_pgtle_$ SELECT 1; $_pgtle_$); + install_extension +------------------- + t +(1 row) + +SELECT pgtle.uninstall_extension('pfx', '1.0'); + uninstall_extension +--------------------- + t +(1 row) + +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('pfx', 'pfx_b') ORDER BY name; + name +------- + pfx_b +(1 row) + +-- clean up the remaining collision-test artifacts +SELECT pgtle.uninstall_extension('col_axb'); + uninstall_extension +--------------------- + t +(1 row) + +SELECT pgtle.uninstall_extension('pfx_b'); + uninstall_extension +--------------------- + t +(1 row) + -- Skip TransactionStmts BEGIN; SELECT pgtle.available_extension_versions(); diff --git a/test/sql/pg_tle_management.sql b/test/sql/pg_tle_management.sql index 2de2bcf..b2d6537 100644 --- a/test/sql/pg_tle_management.sql +++ b/test/sql/pg_tle_management.sql @@ -610,6 +610,35 @@ SELECT name FROM pgtle.available_extensions() WHERE name = 'n5456789012345678901234567890123456789012345678901234'; SELECT pgtle.uninstall_extension('n5456789012345678901234567890123456789012345678901234'); +-- uninstall must not remove another extension whose name it is a prefix of, nor treat '_' as a wildcard +-- set up a prefix target with an update path, a longer-named sibling, and an underscore decoy +SELECT pgtle.install_extension('col_a', '1.0', 'prefix', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.install_extension_version_sql('col_a', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.install_update_path('col_a', '1.0', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.install_extension('col_a_b', '1.0', 'longer sibling', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.install_extension('col_axb', '1.0', 'underscore decoy', $_pgtle_$ SELECT 1; $_pgtle_$); + +-- prefix uninstall removes only col_a's artifacts; the sibling and decoy survive +SELECT pgtle.uninstall_extension('col_a'); +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name; + +-- the '_' in col_a_b must not act as a wildcard and sweep up col_axb +SELECT pgtle.uninstall_extension('col_a_b'); +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name; + +-- the (name, version) form drops the control function by exact name, not by prefix, so pfx_b survives +SELECT pgtle.install_extension('pfx', '1.0', 'prefix2', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.install_extension('pfx_b', '1.0', 'sibling2', $_pgtle_$ SELECT 1; $_pgtle_$); +SELECT pgtle.uninstall_extension('pfx', '1.0'); +SELECT name FROM pgtle.available_extensions() + WHERE name IN ('pfx', 'pfx_b') ORDER BY name; + +-- clean up the remaining collision-test artifacts +SELECT pgtle.uninstall_extension('col_axb'); +SELECT pgtle.uninstall_extension('pfx_b'); + -- Skip TransactionStmts BEGIN; SELECT pgtle.available_extension_versions();