Skip to content

Commit 0082d99

Browse files
jnasbyupgradeclaude
andcommitted
Fix uninstall_extension dropping prefix-named sibling extensions
uninstall_extension() matched the functions to drop with LIKE patterns of the forms <extname>%.control and <extname>%.sql. Because the % immediately follows the name, the patterns also matched any other extension whose name has <extname> as a prefix, so uninstalling 'foo' silently dropped the registration of 'foo_bar'. Names may also contain '_' (a LIKE wildcard), and uninstall_extension('') matched every extension's control function. Match the control function by exact name and the script functions with an anchored, metacharacter-escaped regular expression, delivered as a new 1.5.2 -> 1.5.3 upgrade script that replaces both uninstall_extension overloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 92f908b commit 0082d99

6 files changed

Lines changed: 294 additions & 15 deletions

File tree

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
EXTENSION = pg_tle
2-
EXTVERSION = 1.5.2
2+
EXTVERSION = 1.5.3
33

44
SCHEMA = pgtle
55
MODULE_big = $(EXTENSION)
@@ -9,7 +9,8 @@ OBJS = src/tleextension.o src/guc-file.o src/feature.o src/passcheck.o src/uni_a
99
EXTRA_CLEAN = src/guc-file.c pg_tle.control pg_tle--$(EXTVERSION).sql
1010
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 \
1111
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 \
12-
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
12+
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 \
13+
pg_tle--1.5.2--1.5.3.sql
1314

1415
TESTS = $(wildcard test/sql/*.sql)
1516
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ There are examples for writing TLEs in several languages, including:
3535

3636
## Supported PostgreSQL versions
3737

38-
`pg_tle` 1.5.2 supports PostgreSQL major versions 12 to 18.
38+
`pg_tle` 1.5.3 supports PostgreSQL major versions 12 to 18.
3939

4040
`pg_tle` 1.5.0 supports PostgreSQL major versions 12 to 17.
4141

pg_tle--1.5.2--1.5.3.sql

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
18+
\echo Use "CREATE EXTENSION pg_tle" to load this file. \quit
19+
20+
CREATE OR REPLACE FUNCTION pgtle.uninstall_extension(extname text)
21+
RETURNS boolean
22+
SET search_path TO 'pgtle'
23+
AS $_pgtleie_$
24+
DECLARE
25+
ctlname text;
26+
sqlpattern text;
27+
searchctl text;
28+
searchsql text;
29+
dropsql text;
30+
pgtlensp text := 'pgtle';
31+
func text;
32+
existsvar record;
33+
BEGIN
34+
35+
-- the control function is named exactly '<extname>.control'
36+
ctlname := extname || '.control';
37+
/*
38+
* Script functions are named '<extname>--<version>.sql' and
39+
* '<extname>--<from>--<to>.sql'. Match them with a regex anchored on the
40+
* '--' that follows the name: '^<extname>--<anything>.sql$'. '--' cannot
41+
* occur in an extension name, so a prefix-named sibling like '<extname>_x'
42+
* cannot match. regexp_replace() backslash-escapes any regex
43+
* metacharacters in extname so it is matched literally.
44+
*/
45+
sqlpattern := '^' || regexp_replace(extname, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g') || '--.*\.sql$';
46+
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';
47+
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';
48+
49+
EXECUTE searchctl USING ctlname, pgtlensp INTO existsvar;
50+
IF existsvar IS NULL THEN
51+
RAISE EXCEPTION 'Extension % does not exist', extname USING ERRCODE = 'no_data_found';
52+
ELSE
53+
FOR func IN EXECUTE searchctl USING ctlname, pgtlensp LOOP
54+
dropsql := format('DROP FUNCTION %I()', func);
55+
EXECUTE dropsql;
56+
END LOOP;
57+
END IF;
58+
59+
EXECUTE searchsql USING sqlpattern, pgtlensp INTO existsvar;
60+
IF existsvar IS NULL THEN
61+
RAISE WARNING 'Extension % has an anomaly; control function exists, but no sql commands function exists', extname;
62+
ELSE
63+
FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP
64+
dropsql := format('DROP FUNCTION %I()', func);
65+
EXECUTE dropsql;
66+
END LOOP;
67+
END IF;
68+
69+
RETURN true;
70+
END;
71+
$_pgtleie_$
72+
LANGUAGE plpgsql STRICT;
73+
74+
-- uninstall an extension for a specific version
75+
CREATE OR REPLACE FUNCTION pgtle.uninstall_extension(extname text, version text)
76+
RETURNS boolean
77+
SET search_path TO 'pgtle'
78+
AS $_pgtleie_$
79+
DECLARE
80+
ctlname text;
81+
sqlpattern text;
82+
countverssql text;
83+
vers_count bigint;
84+
defaultversql text;
85+
defaultver text;
86+
searchctl text;
87+
searchsql text;
88+
dropsql text;
89+
pgtlensp text := 'pgtle';
90+
func_available_vers text := 'available_extension_versions()';
91+
func_available_ext text := 'available_extensions()';
92+
func text;
93+
esc_ext text;
94+
esc_ver text;
95+
BEGIN
96+
-- regex-escape name and version so any metacharacters are matched literally
97+
esc_ext := regexp_replace(extname, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g');
98+
esc_ver := regexp_replace(version, '([\\^$.|?*+(){}\[\]-])', '\\\1', 'g');
99+
ctlname := extname || '.control';
100+
/*
101+
* Match this version's install script ('<ext>--<ver>.sql') and every
102+
* update-path script that uses the version as a token, via the alternation
103+
* '(<ver> | .*--<ver> | <ver>--.*)':
104+
* <ver> -> '<ext>--<ver>.sql' (the version install script)
105+
* .*--<ver> -> '<ext>--<from>--<ver>.sql' (an update into this version)
106+
* <ver>--.* -> '<ext>--<ver>--<to>.sql' (an update out of this version)
107+
* '--' separates the tokens and cannot occur within a name or a version.
108+
*/
109+
sqlpattern := '^' || esc_ext || '--(' || esc_ver || '|.*--' || esc_ver || '|' || esc_ver || '--.*)\.sql$';
110+
countverssql := format('SELECT COUNT(*) FROM %s.%s WHERE name = $1', pgtlensp, func_available_vers);
111+
defaultversql := format('SELECT default_version FROM %s.%s WHERE name = $1', pgtlensp, func_available_ext);
112+
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';
113+
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';
114+
115+
EXECUTE countverssql USING extname INTO vers_count;
116+
EXECUTE defaultversql USING extname INTO defaultver;
117+
118+
IF vers_count > 1 THEN
119+
-- if multiple versions exist and this is the default version, don't uninstall
120+
IF version = defaultver THEN
121+
RAISE EXCEPTION 'Can not uninstall default version of extension %, use set_default_version to update the default to another available version and retry', extname;
122+
ELSE
123+
-- remove the specified version sql file function only, don't remove control file function
124+
FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP
125+
dropsql := format('DROP FUNCTION %I()', func);
126+
EXECUTE dropsql;
127+
END LOOP;
128+
END IF;
129+
ELSE
130+
-- check that the specified version matches the only version that exists
131+
-- if it does then uninstall the extension completely
132+
-- if it doesn't then don't uninstall anything to avoid accidental uninstall
133+
IF version = defaultver THEN
134+
FOR func IN EXECUTE searchctl USING ctlname, pgtlensp LOOP
135+
dropsql := format('DROP FUNCTION %I()', func);
136+
EXECUTE dropsql;
137+
END LOOP;
138+
FOR func IN EXECUTE searchsql USING sqlpattern, pgtlensp LOOP
139+
dropsql := format('DROP FUNCTION %I()', func);
140+
EXECUTE dropsql;
141+
END LOOP;
142+
ELSE
143+
RAISE EXCEPTION 'Version % of extension % is not installed and therefore can not be uninstalled', extname, version;
144+
END IF;
145+
END IF;
146+
147+
RETURN TRUE;
148+
END;
149+
$_pgtleie_$
150+
LANGUAGE plpgsql STRICT;

test/expected/pg_tle_functions_acl.out

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,17 @@ SELECT pgtle.set_default_version('', '');
181181
ERROR: invalid extension name: ""
182182
DETAIL: Extension names must not be empty.
183183
SELECT pgtle.uninstall_extension('');
184-
ERROR: must be owner of function test_ext.control
185-
CONTEXT: SQL statement "DROP FUNCTION "test_ext.control"()"
186-
PL/pgSQL function uninstall_extension(text) line 22 at EXECUTE
184+
ERROR: Extension does not exist
185+
CONTEXT: PL/pgSQL function uninstall_extension(text) line 23 at RAISE
187186
SELECT pgtle.uninstall_extension('', '');
188187
ERROR: Version of extension is not installed and therefore can not be uninstalled
189-
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 50 at RAISE
188+
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 61 at RAISE
190189
SELECT pgtle.uninstall_extension_if_exists('');
191-
ERROR: must be owner of function test_ext.control
192-
CONTEXT: SQL statement "DROP FUNCTION "test_ext.control"()"
193-
PL/pgSQL function uninstall_extension(text) line 22 at EXECUTE
194-
SQL statement "SELECT pgtle.uninstall_extension(extname)"
195-
PL/pgSQL function uninstall_extension_if_exists(text) line 3 at PERFORM
190+
uninstall_extension_if_exists
191+
-------------------------------
192+
f
193+
(1 row)
194+
196195
SELECT pgtle.uninstall_update_path('', '', '');
197196
ERROR: Extension does not exist
198197
CONTEXT: PL/pgSQL function uninstall_update_path(text,text,text) line 16 at RAISE

test/expected/pg_tle_management.out

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ SELECT pgtle.uninstall_extension('broken_ext');
586586
-- error
587587
SELECT pgtle.uninstall_extension('bogus');
588588
ERROR: Extension bogus does not exist
589-
CONTEXT: PL/pgSQL function uninstall_extension(text) line 18 at RAISE
589+
CONTEXT: PL/pgSQL function uninstall_extension(text) line 29 at RAISE
590590
-- uninstall_if_exists with a non-existent extension
591591
-- returns false, no error
592592
SELECT pgtle.uninstall_extension_if_exists('bogus');
@@ -716,7 +716,7 @@ SELECT pgtle.available_extension_versions();
716716
-- fails
717717
SELECT pgtle.uninstall_extension('test42', '1.0');
718718
ERROR: Can not uninstall default version of extension test42, use set_default_version to update the default to another available version and retry
719-
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 28 at RAISE
719+
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 44 at RAISE
720720
SELECT pgtle.available_extension_versions();
721721
available_extension_versions
722722
---------------------------------------------------
@@ -742,7 +742,7 @@ SELECT pgtle.available_extension_versions();
742742
-- fails
743743
SELECT pgtle.uninstall_extension('test42', '3.0');
744744
ERROR: Version test42 of extension 3.0 is not installed and therefore can not be uninstalled
745-
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 50 at RAISE
745+
CONTEXT: PL/pgSQL function uninstall_extension(text,text) line 66 at RAISE
746746
SELECT pgtle.available_extension_versions();
747747
available_extension_versions
748748
---------------------------------------------------
@@ -951,6 +951,106 @@ SELECT pgtle.uninstall_extension('n545678901234567890123456789012345678901234567
951951
t
952952
(1 row)
953953

954+
-- uninstall must not remove another extension whose name it is a prefix of, nor treat '_' as a wildcard
955+
-- set up a prefix target with an update path, a longer-named sibling, and an underscore decoy
956+
SELECT pgtle.install_extension('col_a', '1.0', 'prefix', $_pgtle_$ SELECT 1; $_pgtle_$);
957+
install_extension
958+
-------------------
959+
t
960+
(1 row)
961+
962+
SELECT pgtle.install_extension_version_sql('col_a', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$);
963+
install_extension_version_sql
964+
-------------------------------
965+
t
966+
(1 row)
967+
968+
SELECT pgtle.install_update_path('col_a', '1.0', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$);
969+
install_update_path
970+
---------------------
971+
t
972+
(1 row)
973+
974+
SELECT pgtle.install_extension('col_a_b', '1.0', 'longer sibling', $_pgtle_$ SELECT 1; $_pgtle_$);
975+
install_extension
976+
-------------------
977+
t
978+
(1 row)
979+
980+
SELECT pgtle.install_extension('col_axb', '1.0', 'underscore decoy', $_pgtle_$ SELECT 1; $_pgtle_$);
981+
install_extension
982+
-------------------
983+
t
984+
(1 row)
985+
986+
-- prefix uninstall removes only col_a's artifacts; the sibling and decoy survive
987+
SELECT pgtle.uninstall_extension('col_a');
988+
uninstall_extension
989+
---------------------
990+
t
991+
(1 row)
992+
993+
SELECT name FROM pgtle.available_extensions()
994+
WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name;
995+
name
996+
---------
997+
col_a_b
998+
col_axb
999+
(2 rows)
1000+
1001+
-- the '_' in col_a_b must not act as a wildcard and sweep up col_axb
1002+
SELECT pgtle.uninstall_extension('col_a_b');
1003+
uninstall_extension
1004+
---------------------
1005+
t
1006+
(1 row)
1007+
1008+
SELECT name FROM pgtle.available_extensions()
1009+
WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name;
1010+
name
1011+
---------
1012+
col_axb
1013+
(1 row)
1014+
1015+
-- the (name, version) form drops the control function by exact name, not by prefix, so pfx_b survives
1016+
SELECT pgtle.install_extension('pfx', '1.0', 'prefix2', $_pgtle_$ SELECT 1; $_pgtle_$);
1017+
install_extension
1018+
-------------------
1019+
t
1020+
(1 row)
1021+
1022+
SELECT pgtle.install_extension('pfx_b', '1.0', 'sibling2', $_pgtle_$ SELECT 1; $_pgtle_$);
1023+
install_extension
1024+
-------------------
1025+
t
1026+
(1 row)
1027+
1028+
SELECT pgtle.uninstall_extension('pfx', '1.0');
1029+
uninstall_extension
1030+
---------------------
1031+
t
1032+
(1 row)
1033+
1034+
SELECT name FROM pgtle.available_extensions()
1035+
WHERE name IN ('pfx', 'pfx_b') ORDER BY name;
1036+
name
1037+
-------
1038+
pfx_b
1039+
(1 row)
1040+
1041+
-- clean up the remaining collision-test artifacts
1042+
SELECT pgtle.uninstall_extension('col_axb');
1043+
uninstall_extension
1044+
---------------------
1045+
t
1046+
(1 row)
1047+
1048+
SELECT pgtle.uninstall_extension('pfx_b');
1049+
uninstall_extension
1050+
---------------------
1051+
t
1052+
(1 row)
1053+
9541054
-- Skip TransactionStmts
9551055
BEGIN;
9561056
SELECT pgtle.available_extension_versions();

test/sql/pg_tle_management.sql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,35 @@ SELECT name FROM pgtle.available_extensions()
610610
WHERE name = 'n5456789012345678901234567890123456789012345678901234';
611611
SELECT pgtle.uninstall_extension('n5456789012345678901234567890123456789012345678901234');
612612

613+
-- uninstall must not remove another extension whose name it is a prefix of, nor treat '_' as a wildcard
614+
-- set up a prefix target with an update path, a longer-named sibling, and an underscore decoy
615+
SELECT pgtle.install_extension('col_a', '1.0', 'prefix', $_pgtle_$ SELECT 1; $_pgtle_$);
616+
SELECT pgtle.install_extension_version_sql('col_a', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$);
617+
SELECT pgtle.install_update_path('col_a', '1.0', '1.1', $_pgtle_$ SELECT 1; $_pgtle_$);
618+
SELECT pgtle.install_extension('col_a_b', '1.0', 'longer sibling', $_pgtle_$ SELECT 1; $_pgtle_$);
619+
SELECT pgtle.install_extension('col_axb', '1.0', 'underscore decoy', $_pgtle_$ SELECT 1; $_pgtle_$);
620+
621+
-- prefix uninstall removes only col_a's artifacts; the sibling and decoy survive
622+
SELECT pgtle.uninstall_extension('col_a');
623+
SELECT name FROM pgtle.available_extensions()
624+
WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name;
625+
626+
-- the '_' in col_a_b must not act as a wildcard and sweep up col_axb
627+
SELECT pgtle.uninstall_extension('col_a_b');
628+
SELECT name FROM pgtle.available_extensions()
629+
WHERE name IN ('col_a', 'col_a_b', 'col_axb') ORDER BY name;
630+
631+
-- the (name, version) form drops the control function by exact name, not by prefix, so pfx_b survives
632+
SELECT pgtle.install_extension('pfx', '1.0', 'prefix2', $_pgtle_$ SELECT 1; $_pgtle_$);
633+
SELECT pgtle.install_extension('pfx_b', '1.0', 'sibling2', $_pgtle_$ SELECT 1; $_pgtle_$);
634+
SELECT pgtle.uninstall_extension('pfx', '1.0');
635+
SELECT name FROM pgtle.available_extensions()
636+
WHERE name IN ('pfx', 'pfx_b') ORDER BY name;
637+
638+
-- clean up the remaining collision-test artifacts
639+
SELECT pgtle.uninstall_extension('col_axb');
640+
SELECT pgtle.uninstall_extension('pfx_b');
641+
613642
-- Skip TransactionStmts
614643
BEGIN;
615644
SELECT pgtle.available_extension_versions();

0 commit comments

Comments
 (0)