From 81bdb1832b7a5b6f46c54d112e03dd1d6d8413ff Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Wed, 6 Nov 2024 12:23:48 -0600 Subject: [PATCH 01/17] Squashed 'pgxntool/' changes from c0af00f..bed3604 bed3604 Fix pg_regress on versions > 12 (#5) (#6) git-subtree-dir: pgxntool git-subtree-split: bed36044679d6b53ad7cd2875272552a4ad6508a --- HISTORY.asc | 3 +++ base.mk | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HISTORY.asc b/HISTORY.asc index b134482..9cb793b 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,5 +1,8 @@ STABLE ------ +== Support 13+ +The `--load-language` option was removed from `pg_regress` in 13. + == Reduce verbosity from test setup As part of this change, you will want to review the changes to test/deps.sql. diff --git a/base.mk b/base.mk index 0634f2e..a976ebb 100644 --- a/base.mk +++ b/base.mk @@ -36,7 +36,7 @@ TEST_SQL_FILES += $(wildcard $(TESTDIR)/sql/*.sql) TEST_RESULT_FILES = $(patsubst $(TESTDIR)/sql/%.sql,$(TESTDIR)/expected/%.out,$(TEST_SQL_FILES)) TEST_FILES = $(TEST_SOURCE_FILES) $(TEST_SQL_FILES) REGRESS = $(sort $(notdir $(subst .source,,$(TEST_FILES:.sql=)))) # Sort is to get unique list -REGRESS_OPTS = --inputdir=$(TESTDIR) --outputdir=$(TESTOUT) --load-language=plpgsql +REGRESS_OPTS = --inputdir=$(TESTDIR) --outputdir=$(TESTOUT) # See additional setup below MODULES = $(patsubst %.c,%,$(wildcard src/*.c)) ifeq ($(strip $(MODULES)),) MODULES =# Set to NUL so PGXS doesn't puke @@ -57,8 +57,10 @@ GE91 = $(call test, $(MAJORVER), -ge, 91) ifeq ($(GE91),yes) all: $(EXTENSION_VERSION_FILES) +endif -#DATA = $(wildcard sql/*--*.sql) +ifeq ($($call test, $(MAJORVER), -lt 13), yes) + REGRESS_OPTS += --load-language=plpgsql endif PGXS := $(shell $(PG_CONFIG) --pgxs) From cb9b8a4207380a6daaf0e3c6f478981a9e172733 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Wed, 8 Apr 2026 16:24:13 -0500 Subject: [PATCH 02/17] Add cat_tools 0.2.2: fix PG11+ and PG12+ compatibility via dynamic views - Add __cat_tools.omit_column() helper to list catalog columns dynamically - Add ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES - Fix _cat_tools.pg_class_v: use omit_column to exclude duplicate oid (PG12+) - Fix _cat_tools.pg_attribute_v: omit attmissingval (anyarray, PG11+), include it cast to text::text[] so the column is preserved across PG versions - Fix cat_tools.pg_extension_v: use omit_column to exclude duplicate oid (PG12+) - Add control.mk to drive version file generation - Add sql/cat_tools--0.2.1--0.2.2.sql upgrade script Co-Authored-By: Claude Sonnet 4.6 --- META.in.json | 4 +- META.json | 4 +- Makefile | 4 + cat_tools.control | 2 +- control.mk | 5 + sql/cat_tools--0.2.1--0.2.2.sql | 258 ++++++++++++++++++++++++++++++++ sql/cat_tools.sql.in | 77 ++++++++-- 7 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 control.mk create mode 100644 sql/cat_tools--0.2.1--0.2.2.sql diff --git a/META.in.json b/META.in.json index cb35668..6f07609 100644 --- a/META.in.json +++ b/META.in.json @@ -14,7 +14,7 @@ "name": "cat_tools", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.1", + "version": "0.2.2", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Tools for interfacing with the Postgres catalog", @@ -37,7 +37,7 @@ "file": "sql/cat_tools.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.1", + "version": "0.2.2", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", diff --git a/META.json b/META.json index ad50c0f..2bd6367 100644 --- a/META.json +++ b/META.json @@ -14,7 +14,7 @@ "name": "cat_tools", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.1", + "version": "0.2.2", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Tools for interfacing with the Postgres catalog", @@ -37,7 +37,7 @@ "file": "sql/cat_tools.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.1", + "version": "0.2.2", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", diff --git a/Makefile b/Makefile index b4faf23..ba0fa29 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ B = sql testdeps: $(wildcard test/*.sql test/helpers/*.sql) # Be careful not to include directories in this include pgxntool/base.mk +include control.mk LT95 = $(call test, $(MAJORVER), -lt, 95) LT93 = $(call test, $(MAJORVER), -lt, 93) @@ -17,6 +18,9 @@ all: $B/cat_tools.sql $(versioned_out) installcheck: $B/cat_tools.sql $(versioned_out) EXTRA_CLEAN += $B/cat_tools.sql $(versioned_out) +# Install historical version scripts so the upgrade test can start from them +DATA += sql/cat_tools--0.2.1.sql + # TODO: refactor the version stuff into a function # # This initially creates $@.tmp before moving it into place atomically. That's diff --git a/cat_tools.control b/cat_tools.control index 5248506..6a6297a 100644 --- a/cat_tools.control +++ b/cat_tools.control @@ -1,4 +1,4 @@ comment = 'Tools for intorfacing with the catalog' -default_version = '0.2.1' +default_version = '0.2.2' relocatable = false schema = 'cat_tools' diff --git a/control.mk b/control.mk new file mode 100644 index 0000000..7882eb8 --- /dev/null +++ b/control.mk @@ -0,0 +1,5 @@ +EXTENSION_cat_tools_VERSION := 0.2.2 +$(EXTENSION_cat_tools_VERSION_FILE): sql/cat_tools.sql cat_tools.control + @echo '/* DO NOT EDIT - AUTO-GENERATED FILE */' > $(EXTENSION_cat_tools_VERSION_FILE) + @cat sql/cat_tools.sql >> $(EXTENSION_cat_tools_VERSION_FILE) + diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql new file mode 100644 index 0000000..a01dedb --- /dev/null +++ b/sql/cat_tools--0.2.1--0.2.2.sql @@ -0,0 +1,258 @@ +CREATE SCHEMA __cat_tools; + +CREATE FUNCTION __cat_tools.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text DEFAULT NULL + , comment text DEFAULT NULL +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); + + create_template CONSTANT text := $template$ +CREATE OR REPLACE FUNCTION %s( +%s +) RETURNS %s AS +%L +$template$ + ; + + revoke_template CONSTANT text := $template$ +REVOKE ALL ON FUNCTION %s( +%s +) FROM public; +$template$ + ; + + grant_template CONSTANT text := $template$ +GRANT EXECUTE ON FUNCTION %s( +%s +) TO %s; +$template$ + ; + + comment_template CONSTANT text := $template$ +COMMENT ON FUNCTION %s( +%s +) IS %L; +$template$ + ; + +BEGIN + PERFORM __cat_tools.exec( format( + create_template + , function_name + , args + , options -- TODO: Force search_path if options ~* 'definer' + , body + ) ) + ; + PERFORM __cat_tools.exec( format( + revoke_template + , function_name + , c_simple_args + ) ) + ; + + IF grants IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + grant_template + , function_name + , c_simple_args + , grants + ) ) + ; + END IF; + + IF comment IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + comment_template + , function_name + , c_simple_args + , comment + ) ) + ; + END IF; +END +$body$; + +CREATE FUNCTION __cat_tools.omit_column( + rel text + , omit name[] DEFAULT array['oid'] +) RETURNS text LANGUAGE sql IMMUTABLE AS $body$ +SELECT array_to_string(array( + SELECT attname + FROM pg_attribute a + WHERE attrelid = rel::regclass + AND NOT attisdropped + AND attnum >= 0 + AND attname != ANY( omit ) + ORDER BY attnum + ) + , ', ' +) +$body$; + +ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES TO cat_tools__usage; + +-- Recreate _cat_tools.pg_class_v with dynamic column list to handle PG12+ oid visibility. +-- WARNING: CASCADE will drop cat_tools.pg_class_v, _cat_tools.pg_attribute_v, +-- _cat_tools.column, and cat_tools.column. Any user-defined views depending on +-- cat_tools.pg_class_v or cat_tools.column must be recreated after this upgrade. +DROP VIEW IF EXISTS _cat_tools.pg_class_v CASCADE; + +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS + SELECT c.oid AS reloid + , %s + , n.nspname AS relschema + FROM pg_class c + LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_class') +)); +REVOKE ALL ON _cat_tools.pg_class_v FROM public; + +CREATE OR REPLACE VIEW cat_tools.pg_class_v AS + SELECT * + FROM _cat_tools.pg_class_v + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND relkind IN( 'r', 'v', 'f' ) +; +GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; + +-- Recreate pg_attribute_v (dropped via pg_class_v CASCADE above). +-- On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). +-- We include it cast to text[] so the column is preserved; on PG10 it doesn't exist. +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS + SELECT %s + %s + , c.* + , t.oid AS typoid + , %s + FROM pg_attribute a + LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) + LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) + , CASE WHEN EXISTS( + SELECT 1 FROM pg_catalog.pg_attribute + WHERE attrelid = 'pg_catalog.pg_attribute'::regclass + AND attname = 'attmissingval' + ) THEN ', a.attmissingval::text::text[]' ELSE '' END + , __cat_tools.omit_column('pg_catalog.pg_type') +)); +REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; + +-- Recreate _cat_tools.column (dropped by pg_class_v CASCADE above). +CREATE OR REPLACE VIEW _cat_tools.column AS + SELECT * + , pg_catalog.format_type(typoid, atttypmod) AS column_type + , CASE typtype + WHEN 'd' THEN pg_catalog.format_type(typbasetype, typtypmod) + WHEN 'e' THEN 'text' + ELSE pg_catalog.format_type(typoid, atttypmod) + END AS base_type + , pk.conkey AS pk_columns + , ARRAY[attnum] <@ pk.conkey AS is_pk_member + , (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) + FROM pg_catalog.pg_attrdef d + WHERE d.adrelid = a.attrelid + AND d.adnum = a.attnum + AND a.atthasdef + ) AS column_default + FROM _cat_tools.pg_attribute_v a + LEFT JOIN pg_constraint pk + ON ( reloid = pk.conrelid ) + AND pk.contype = 'p' +; +REVOKE ALL ON _cat_tools.column FROM public; + +-- Recreate cat_tools.column (dropped by pg_class_v CASCADE above). +CREATE OR REPLACE VIEW cat_tools.column AS + SELECT * + FROM _cat_tools.column + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND attnum > 0 + AND NOT attisdropped + AND relkind IN( 'r', 'v', 'f' ) + AND ( + pg_has_role(SESSION_USER, relowner, 'USAGE'::text) + OR has_column_privilege(SESSION_USER, reloid, attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY relschema, relname, attnum +; +GRANT SELECT ON cat_tools.column TO cat_tools__usage; + +-- Fix cat_tools.pg_extension_v for PG12+ oid visibility. +-- CASCADE is required: pg_extension__get(name) depends on pg_extension_v's row type. +DROP VIEW IF EXISTS cat_tools.pg_extension_v CASCADE; +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS + SELECT e.oid + , %s + , extnamespace::regnamespace AS extschema + , extconfig::pg_catalog.regclass[] AS ext_config_tables + FROM pg_catalog.pg_extension e + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_extension') +)); +GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; + +-- Recreate cat_tools.pg_extension__get (dropped by the CASCADE above). +SELECT __cat_tools.create_function( + 'cat_tools.pg_extension__get' + , 'extension_name name' + , $$cat_tools.pg_extension_v LANGUAGE plpgsql$$ + , $body$ +DECLARE + r cat_tools.pg_extension_v; +BEGIN + SELECT INTO STRICT r + * + FROM cat_tools.pg_extension_v + WHERE extname = extension_name + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'extension "%" does not exist', extension_name + USING ERRCODE = 'undefined_object' + ; +END +$body$ + , 'cat_tools__usage' +); + +-- Drop temporary helper objects +DROP FUNCTION __cat_tools.omit_column( + rel text + , omit name[] -- DEFAULT array['oid'] +); +DROP FUNCTION __cat_tools.exec( + sql text +); +DROP FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text + , comment text +); +DROP SCHEMA __cat_tools; diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index dda82d8..60892ba 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -20,16 +20,9 @@ CREATE SCHEMA __cat_tools; -- Schema already created via CREATE EXTENSION GRANT USAGE ON SCHEMA cat_tools TO cat_tools__usage; +ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES TO cat_tools__usage; CREATE SCHEMA _cat_tools; --- No permissions checks -CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS - SELECT c.oid AS reloid, c.*, n.nspname AS relschema - FROM pg_class c - LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) -; -REVOKE ALL ON _cat_tools.pg_class_v FROM public; - @generated@ CREATE FUNCTION __cat_tools.exec( @@ -41,8 +34,43 @@ BEGIN END $body$; +-- See also test/setup.sql +CREATE FUNCTION __cat_tools.omit_column( + rel text + , omit name[] DEFAULT array['oid'] +) RETURNS text LANGUAGE sql IMMUTABLE AS $body$ +SELECT array_to_string(array( + SELECT attname + FROM pg_attribute a + WHERE attrelid = rel::regclass + AND NOT attisdropped + AND attnum >= 0 + AND attname != ANY( omit ) + ORDER BY attnum + ) + , ', ' +) +$body$; + @generated@ +/* + * Starting in PG12 oid columns in catalog tables are no longer hidden, so we + * need a way to include all the fields in a table *except* for the OID column. + */ +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS + SELECT c.oid AS reloid + , %s + , n.nspname AS relschema + FROM pg_class c + LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_class') +)); +REVOKE ALL ON _cat_tools.pg_class_v FROM public; + /* * Temporary stub function. We do this so we can use the nice create_function * function that we're about to create to create the real version of this @@ -717,15 +745,28 @@ GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; @generated@ +-- On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). +-- We include it cast to text[] so the column is preserved; on PG10 it doesn't exist. +SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS - SELECT a.* + SELECT %s + %s , c.* , t.oid AS typoid - , t.* + , %s FROM pg_attribute a LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) ; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) + , CASE WHEN EXISTS( + SELECT 1 FROM pg_catalog.pg_attribute + WHERE attrelid = 'pg_catalog.pg_attribute'::regclass + AND attname = 'attmissingval' + ) THEN ', a.attmissingval::text::text[]' ELSE '' END + , __cat_tools.omit_column('pg_catalog.pg_type') +)); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; CREATE OR REPLACE VIEW _cat_tools.column AS @@ -755,17 +796,23 @@ REVOKE ALL ON _cat_tools.column FROM public; @generated@ --- No perms on extension visibility +-- Starting in PG12, oid became a visible column in system catalogs. +-- Use omit_column to avoid duplicate oid columns. +SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS - SELECT e.oid, e.* + SELECT e.oid + , %s , extnamespace::regnamespace AS extschema -- SED: REQUIRES 9.5! - , nspname AS extschema -- SED: PRIOR TO 9.5! +-- Not used prior to 9.5: , nspname AS extschema , extconfig::pg_catalog.regclass[] AS ext_config_tables FROM pg_catalog.pg_extension e LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace ; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_extension') +)); GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; CREATE OR REPLACE VIEW cat_tools.column AS @@ -1454,6 +1501,10 @@ CLUSTER _cat_tools.catalog_metadata USING catalog_metadata__pk_object_catalog; /* * Drop "temporary" objects */ +DROP FUNCTION __cat_tools.omit_column( + rel text + , omit name[] +); DROP FUNCTION __cat_tools.exec( sql text ); From 329009499762dde4f570f995ab942fde2d84959c Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 14 Apr 2026 14:46:18 -0500 Subject: [PATCH 03/17] Use block comment format for pg_class_v upgrade warning Co-Authored-By: Claude Sonnet 4.6 --- sql/cat_tools--0.2.1--0.2.2.sql | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql index a01dedb..23d04d7 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql +++ b/sql/cat_tools--0.2.1--0.2.2.sql @@ -106,10 +106,12 @@ $body$; ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES TO cat_tools__usage; --- Recreate _cat_tools.pg_class_v with dynamic column list to handle PG12+ oid visibility. --- WARNING: CASCADE will drop cat_tools.pg_class_v, _cat_tools.pg_attribute_v, --- _cat_tools.column, and cat_tools.column. Any user-defined views depending on --- cat_tools.pg_class_v or cat_tools.column must be recreated after this upgrade. +/* + * Recreate _cat_tools.pg_class_v with dynamic column list to handle PG12+ oid visibility. + * WARNING: CASCADE will drop cat_tools.pg_class_v, _cat_tools.pg_attribute_v, + * _cat_tools.column, and cat_tools.column. Any user-defined views depending on + * cat_tools.pg_class_v or cat_tools.column must be recreated after this upgrade. + */ DROP VIEW IF EXISTS _cat_tools.pg_class_v CASCADE; SELECT __cat_tools.exec(format($fmt$ From bdb5068e2cb48204453efb7a665ef5f192738d27 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 14 Apr 2026 14:55:50 -0500 Subject: [PATCH 04/17] Fix attmissingval: consistent view schema across PG versions - Use /* */ block comments (was --) - pg_attribute_v always includes attmissingval column as text[]: NULL::text[] on PG10 (column absent), a.attmissingval::text::text[] on PG11+. Uses hardcoded OID 1249 (pg_catalog.pg_attribute). - Add committed sql/cat_tools--0.2.1.sql for upgrade test Co-Authored-By: Claude Sonnet 4.6 --- sql/cat_tools--0.2.1--0.2.2.sql | 17 +- sql/cat_tools--0.2.1.sql | 1472 +++++++++++++++++++++++++++++++ sql/cat_tools.sql.in | 21 +- 3 files changed, 1497 insertions(+), 13 deletions(-) create mode 100644 sql/cat_tools--0.2.1.sql diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql index 23d04d7..852d600 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql +++ b/sql/cat_tools--0.2.1--0.2.2.sql @@ -135,13 +135,16 @@ CREATE OR REPLACE VIEW cat_tools.pg_class_v AS ; GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; --- Recreate pg_attribute_v (dropped via pg_class_v CASCADE above). --- On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). --- We include it cast to text[] so the column is preserved; on PG10 it doesn't exist. +/* + * Recreate pg_attribute_v (dropped via pg_class_v CASCADE above). + * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). + * Always include it as text[] — NULL on PG10 (column absent), real value on PG11+. + * This ensures consistent view schema across all PG versions. + */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS SELECT %s - %s + , %s AS attmissingval , c.* , t.oid AS typoid , %s @@ -153,9 +156,11 @@ $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) , CASE WHEN EXISTS( SELECT 1 FROM pg_catalog.pg_attribute - WHERE attrelid = 'pg_catalog.pg_attribute'::regclass + WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 AND attname = 'attmissingval' - ) THEN ', a.attmissingval::text::text[]' ELSE '' END + ) THEN 'a.attmissingval::text::text[]' + ELSE 'NULL::text[]' + END , __cat_tools.omit_column('pg_catalog.pg_type') )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; diff --git a/sql/cat_tools--0.2.1.sql b/sql/cat_tools--0.2.1.sql new file mode 100644 index 0000000..d9f5772 --- /dev/null +++ b/sql/cat_tools--0.2.1.sql @@ -0,0 +1,1472 @@ +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SET LOCAL client_min_messages = WARNING; + +DO $$ +BEGIN + CREATE ROLE cat_tools__usage NOLOGIN; +EXCEPTION WHEN duplicate_object THEN + NULL; +END +$$; + +/* + * NOTE: All pg_temp objects must be dropped at the end of the script! + * Otherwise the eventual DROP CASCADE of pg_temp when the session ends will + * also drop the extension! Instead of risking problems, create our own + * "temporary" schema instead. + */ +CREATE SCHEMA __cat_tools; + +-- Schema already created via CREATE EXTENSION +GRANT USAGE ON SCHEMA cat_tools TO cat_tools__usage; +CREATE SCHEMA _cat_tools; + +-- No permissions checks +CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS + SELECT c.oid AS reloid, c.*, n.nspname AS relschema + FROM pg_class c + LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) +; +REVOKE ALL ON _cat_tools.pg_class_v FROM public; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE FUNCTION __cat_tools.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +/* + * Temporary stub function. We do this so we can use the nice create_function + * function that we're about to create to create the real version of this + * function. + */ +CREATE FUNCTION cat_tools.function__arg_types_text(text +) RETURNS text LANGUAGE sql AS 'SELECT $1'; + +CREATE FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text DEFAULT NULL + , comment text DEFAULT NULL +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); + + create_template CONSTANT text := $template$ +CREATE OR REPLACE FUNCTION %s( +%s +) RETURNS %s AS +%L +$template$ + ; + + revoke_template CONSTANT text := $template$ +REVOKE ALL ON FUNCTION %s( +%s +) FROM public; +$template$ + ; + + grant_template CONSTANT text := $template$ +GRANT EXECUTE ON FUNCTION %s( +%s +) TO %s; +$template$ + ; + + comment_template CONSTANT text := $template$ +COMMENT ON FUNCTION %s( +%s +) IS %L; +$template$ + ; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +BEGIN + PERFORM __cat_tools.exec( format( + create_template + , function_name + , args + , options -- TODO: Force search_path if options ~* 'definer' + , body + ) ) + ; + PERFORM __cat_tools.exec( format( + revoke_template + , function_name + , c_simple_args + ) ) + ; + + IF grants IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + grant_template + , function_name + , c_simple_args + , grants + ) ) + ; + END IF; + + IF comment IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + comment_template + , function_name + , c_simple_args + , comment + ) ) + ; + END IF; +END +$body$; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + input_arg_types pg_catalog.regtype[]; + + c_template CONSTANT text := $fmt$CREATE FUNCTION pg_temp.cat_tools__function__arg_types__temp_function( + %s + ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN NULL; END' + $fmt$; + + temp_proc pg_catalog.regprocedure; + sql text; +BEGIN + sql := format( + c_template + , arguments + , 'void' + ); + --RAISE DEBUG 'Executing SQL %', sql; + DECLARE + v_type pg_catalog.regtype; + BEGIN + EXECUTE sql; + EXCEPTION WHEN invalid_function_definition THEN + v_type := (regexp_matches( SQLERRM, 'function result type must be ([^ ]+) because of' ))[1]; + sql := format( + c_template + , arguments + , v_type + ); + EXECUTE sql; + END; + + /* + * Get new OID. *This must be done dynamically!* Otherwise we get stuck + * with a CONST oid after first compilation. The regproc cast ensures there's + * only one function with this name. The cast to regprocedure is for the sake + * of the DROP down below. + */ + EXECUTE $$SELECT 'pg_temp.cat_tools__function__arg_types__temp_function'::pg_catalog.regproc::pg_catalog.regprocedure$$ INTO temp_proc; + SELECT INTO STRICT input_arg_types + -- This is here to re-cast the array as 1-based instead of 0 based (better solutions welcome!) + string_to_array(proargtypes::text,' ')::pg_catalog.regtype[] + FROM pg_proc + WHERE oid = temp_proc + ; + -- NOTE: DROP may not accept all the argument options that CREATE does, so use temp_proc + EXECUTE format( + $fmt$DROP FUNCTION %s$fmt$ + , temp_proc + ); + + RETURN input_arg_types; +END +$body$ + , 'cat_tools__usage' + , 'Returns argument types for a function argument body as an array. Unlike a + normal regprocedure cast, this function accepts anything that is valid when + defining a function.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.function__arg_types($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns argument types for a function argument body as text. Unlike a + normal regprocedure cast, this function accepts anything that is valid when + defining a function.' + +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.regprocedure' + , $$ + function_name text + , arguments text$$ + , $$pg_catalog.regprocedure LANGUAGE sql$$ + , $body$ +SELECT format( + '%s(%s)' + , $1 + , cat_tools.function__arg_types_text($2) +)::pg_catalog.regprocedure +$body$ + , 'cat_tools__usage' + , 'Returns a regprocedure for a given function name and arguments. Unlike a + normal regprocedure cast, arguments can contain anything that is valid when + defining a function.' +); + + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE TYPE cat_tools.constraint_type AS ENUM( + 'domain constraint', 'table constraint' +); +COMMENT ON TYPE cat_tools.constraint_type IS $$Descriptive names for every type of Postgres object (table, operator, rule, etc)$$; +CREATE TYPE cat_tools.procedure_type AS ENUM( + 'aggregate', 'function' +); +COMMENT ON TYPE cat_tools.procedure_type IS $$Types of constraints (`domain constraint` or `table_constraint`)$$; + +CREATE TYPE cat_tools.relation_type AS ENUM( + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' +); +COMMENT ON TYPE cat_tools.relation_type IS $$Types of objects stored in `pg_class`$$; + +CREATE TYPE cat_tools.relation_relkind AS ENUM( + 'r' + , 'i' + , 'S' + , 't' + , 'v' + , 'c' + , 'f' + , 'm' +); +COMMENT ON TYPE cat_tools.relation_relkind IS $$Valid values for `pg_class.relkind`$$; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE TYPE cat_tools.object_type AS ENUM( + -- pg_class + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' + /* + * NOTE! These are a bit weird because columns live in pg_attribute, but + * address stuff recognizes columns as part of pg_class with a subobjid <> 0! + */ + , 'table column' + , 'index column' + , 'sequence column' + , 'toast table column' + , 'view column' + , 'materialized view column' + , 'composite type column' + , 'foreign table column' + -- pg_constraint + -- NOTE: a domain itself is considered to be a type + , 'domain constraint', 'table constraint' + -- pg_proc + , 'aggregate', 'function' + -- This is taken from getObjectTypeDescription() in objectaddress.c in the Postgres source code + , 'type' + , 'cast' + , 'collation' + , 'conversion' + , 'default value' -- pg_attrdef + , 'language' + , 'large object' -- pg_largeobject + , 'operator' + , 'operator class' -- pg_opclass + , 'operator family' -- pg_opfamily + , 'operator of access method' -- pg_amop + , 'function of access method' -- pg_amproc + , 'rule' -- pg_rewrite + , 'trigger' + , 'schema' -- pg_namespace + , 'text search parser' -- pg_ts_parser + , 'text search dictionary' -- pg_ts_dict + , 'text search template' -- pg_ts_template + , 'text search configuration' -- pg_ts_config + , 'role' -- pg_authid + , 'database' + , 'tablespace' + , 'foreign-data wrapper' -- pg_foreign_data_wrapper + , 'server' -- pg_foreign_server + , 'user mapping' -- pg_user_mapping + , 'default acl' -- pg_default_acl + , 'extension' + , 'event trigger' -- pg_event_trigger -- SED: REQUIRES 9.3! + , 'policy' -- SED: REQUIRES 9.5! + , 'transform' -- SED: REQUIRES 9.5! + , 'access method' -- pg_am +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.objects__shared' + , '' + , 'cat_tools.object_type[] LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT '{role,database,tablespace}'::cat_tools.object_type[] +$body$ + , 'cat_tools__usage' + , 'Returns array of object types for shared objects.' +); +SELECT __cat_tools.create_function( + 'cat_tools.objects__shared_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT * FROM pg_catalog.unnest(cat_tools.objects__shared()) +$body$ + , 'cat_tools__usage' + , 'Returns set of object types for shared objects.' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_shared' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT object_type = ANY(cat_tools.objects__shared()) +$body$ + , 'cat_tools__usage' + , 'Returns true if object_type is a shared object.' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_shared' + , 'object_type text' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT cat_tools.object__is_shared(object_type::cat_tools.object_type) +$body$ + , 'cat_tools__usage' + , 'Returns true if object_type is a shared object.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.objects__address_unsupported' + , '' + , 'cat_tools.object_type[] LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT array[ + 'toast table'::cat_tools.object_type, 'composite type' + , 'index column' , 'sequence column', 'toast table column', 'view column' + , 'materialized view column', 'composite type column' +] +$body$ + , 'cat_tools__usage' + , 'Returns array of object types not supported by pg_get_object_address().' +); +SELECT __cat_tools.create_function( + 'cat_tools.objects__address_unsupported_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT * FROM pg_catalog.unnest(cat_tools.objects__address_unsupported()) +$body$ + , 'cat_tools__usage' + , 'Returns set of object types not supported by pg_get_object_address().' +); +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in +SELECT __cat_tools.create_function( + 'cat_tools.object__is_address_unsupported' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT object_type = ANY(cat_tools.objects__address_unsupported()) +$body$ + , 'cat_tools__usage' + , 'Returns true if object type is not supported by pg_get_object_address().' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_address_unsupported' + , 'object_type text' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT cat_tools.object__is_address_unsupported(object_type::cat_tools.object_type) +$body$ + , 'cat_tools__usage' + , 'Returns true if object type is not supported by pg_get_object_address().' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.object__catalog' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT ( + 'pg_catalog.' + || CASE + WHEN object_type = ANY( array[ + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' + ]::cat_tools.object_type[] ) + THEN 'pg_class' + WHEN object_type = ANY( '{domain constraint,table constraint}'::cat_tools.object_type[] ) + THEN 'pg_constraint' + WHEN object_type = ANY( '{aggregate,function}'::cat_tools.object_type[] ) + THEN 'pg_proc' + WHEN object_type::text LIKE '% column' + THEN 'pg_attribute' + ELSE CASE object_type + -- Unusual cases + -- s/, \(.\{-}\) -- \(.*\)/ WHEN \1 THEN '\2'/ + WHEN 'default value' THEN 'pg_attrdef' + WHEN 'large object' THEN 'pg_largeobject' + WHEN 'operator class' THEN 'pg_opclass' + WHEN 'operator family' THEN 'pg_opfamily' + WHEN 'operator of access method' THEN 'pg_amop' + WHEN 'function of access method' THEN 'pg_amproc' + WHEN 'rule' THEN 'pg_rewrite' + WHEN 'schema' THEN 'pg_namespace' + WHEN 'text search parser' THEN 'pg_ts_parser' + WHEN 'text search dictionary' THEN 'pg_ts_dict' + WHEN 'text search template' THEN 'pg_ts_template' + WHEN 'text search configuration' THEN 'pg_ts_config' + WHEN 'role' THEN 'pg_authid' + WHEN 'foreign-data wrapper' THEN 'pg_foreign_data_wrapper' + WHEN 'server' THEN 'pg_foreign_server' + WHEN 'user mapping' THEN 'pg_user_mapping' + WHEN 'default acl' THEN 'pg_default_acl' + WHEN 'event trigger' THEN 'pg_event_trigger' -- SED: REQUIRES 9.3! + WHEN 'access method' THEN 'pg_am' + ELSE 'pg_' || object_type::text + END + END + )::pg_catalog.regclass +$body$ + , 'cat_tools__usage' + , 'Returns catalog table that is used to store objects' +); +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in +SELECT __cat_tools.create_function( + 'cat_tools.object__catalog' + , 'object_type text' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__catalog(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns catalog table that is used to store objects' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.object__address_classid' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT CASE + WHEN c = 'pg_catalog.pg_attribute'::regclass THEN 'pg_catalog.pg_class'::regclass + ELSE c + END + FROM cat_tools.object__catalog(object_type::cat_tools.object_type) c +$body$ + , 'cat_tools__usage' + , 'Returns the classid used by the pg_*_object*() functions for an object_type' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__address_classid' + , 'object_type text' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__address_classid(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns the classid used by the pg_*_object*() functions for an object_type' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE TABLE _cat_tools.catalog_metadata( + object_catalog pg_catalog.regclass + CONSTRAINT catalog_metadata__pk_object_catalog PRIMARY KEY + , namespace_field name + , reg_type pg_catalog.regtype + , simple_reg_type pg_catalog.regtype +); +-- Table is populated later, after enum_range_srf is created +SELECT __cat_tools.create_function( + '_cat_tools.catalog_metadata__get' + , 'object_catalog _cat_tools.catalog_metadata.object_catalog%TYPE' + , '_cat_tools.catalog_metadata LANGUAGE plpgsql IMMUTABLE' -- Technically should be STABLE + , $body$ +DECLARE + o _cat_tools.catalog_metadata; +BEGIN + SELECT INTO STRICT o + * + FROM _cat_tools.catalog_metadata m + WHERE m.object_catalog = catalog_metadata__get.object_catalog + ; + RETURN o; +END +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_catalog pg_catalog.regclass' + , 'pg_catalog.regtype LANGUAGE sql SECURITY DEFINER STRICT IMMUTABLE' + , $body$ +SELECT (_cat_tools.catalog_metadata__get(object_catalog)).reg_type +$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regtype LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__reg_type(cat_tools.object__catalog(object_type))$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_type text' + , 'pg_catalog.regtype LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__reg_type(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type_catalog' + , 'object_identifier_type regtype' + , 'pg_catalog.regclass LANGUAGE plpgsql SECURITY DEFINER STRICT IMMUTABLE' + , $body$ +DECLARE + cat pg_catalog.regclass; +BEGIN + SELECT INTO STRICT cat + object_catalog + FROM _cat_tools.catalog_metadata m + WHERE m.reg_type = object_identifier_type + OR m.simple_reg_type = object_identifier_type + ; + RETURN cat; +EXCEPTION WHEN no_data_found THEN + IF object_identifier_type::text LIKE 'reg%' THEN + RAISE 'object identifier type % is not supported', object_identifier_type + USING + HINT = format( 'If %I is a valid object identifier type please open an issue at https://github.com/decibel/cat_tools/issues.', object_identifier_type ) + , ERRCODE = 'feature_not_supported' + ; + ELSE + RAISE '% is not a object identifier type', object_identifier_type + USING + HINT = 'See https://www.postgresql.org/docs/current/static/datatype-oid.html' + , ERRCODE = 'wrong_object_type' + ; + END IF; +END +$body$ + , 'cat_tools__usage' + , 'Returns the system catalog that stores a particular object identifier type.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind cat_tools.relation_relkind' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT CASE relkind + WHEN 'r' THEN 'table' + WHEN 'i' THEN 'index' + WHEN 'S' THEN 'sequence' + WHEN 't' THEN 'toast table' + WHEN 'v' THEN 'view' + WHEN 'c' THEN 'materialized view' + WHEN 'f' THEN 'composite type' + WHEN 'm' THEN 'foreign table' +END::cat_tools.relation_type +$body$ + , 'cat_tools__usage' + , 'Mapping from to a ' +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__relkind' + , 'kind cat_tools.relation_type' + , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT CASE kind + WHEN 'table' THEN 'r' + WHEN 'index' THEN 'i' + WHEN 'sequence' THEN 'S' + WHEN 'toast table' THEN 't' + WHEN 'view' THEN 'v' + WHEN 'materialized view' THEN 'c' + WHEN 'composite type' THEN 'f' + WHEN 'foreign table' THEN 'm' +END::cat_tools.relation_relkind +$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.relation__relkind' + , 'kind text' + , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.relation__relkind(kind::cat_tools.relation_type)$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind text' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.relation__kind(relkind::cat_tools.relation_relkind)$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE OR REPLACE VIEW _cat_tools.pg_depend_identity_v AS -- SED: REQUIRES 9.3! + SELECT o.type AS object_type -- SED: REQUIRES 9.3! + , o.schema AS object_schema -- SED: REQUIRES 9.3! + , o.name AS object_name -- SED: REQUIRES 9.3! + , o.identity AS object_identity -- SED: REQUIRES 9.3! + , r.type AS reference_type -- SED: REQUIRES 9.3! + , r.schema AS reference_schema -- SED: REQUIRES 9.3! + , r.name AS reference_name -- SED: REQUIRES 9.3! + , r.identity AS reference_identity -- SED: REQUIRES 9.3! + , d.* -- SED: REQUIRES 9.3! + FROM pg_catalog.pg_depend d -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(classid, objid, objsubid) o -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(refclassid, refobjid, refobjsubid) r -- SED: REQUIRES 9.3! + WHERE classid <> 0 -- SED: REQUIRES 9.3! + UNION ALL -- SED: REQUIRES 9.3! + SELECT NULL, NULL, NULL, NULL -- SED: REQUIRES 9.3! + , r.type AS reference_type -- SED: REQUIRES 9.3! + , r.schema AS reference_schema -- SED: REQUIRES 9.3! + , r.name AS reference_name -- SED: REQUIRES 9.3! + , r.identity AS reference_identity -- SED: REQUIRES 9.3! + , d.* -- SED: REQUIRES 9.3! + FROM pg_catalog.pg_depend d -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(refclassid, refobjid, refobjsubid) r -- SED: REQUIRES 9.3! + WHERE classid = 0 -- SED: REQUIRES 9.3! +; -- SED: REQUIRES 9.3! + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE OR REPLACE VIEW cat_tools.pg_class_v AS + SELECT * + FROM _cat_tools.pg_class_v + + /* + * Oddly, there's no security associated with schema or table visibility. + * Be a bit paranoid though. + */ + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND relkind IN( 'r', 'v', 'f' ) +; +GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS + SELECT a.* + , c.* + , t.oid AS typoid + , t.* + FROM pg_attribute a + LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) + LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) +; +REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; + +CREATE OR REPLACE VIEW _cat_tools.column AS + SELECT * + , pg_catalog.format_type(typoid, atttypmod) AS column_type + , CASE typtype + -- domain + WHEN 'd' THEN pg_catalog.format_type(typbasetype, typtypmod) + -- enum + WHEN 'e' THEN 'text' + ELSE pg_catalog.format_type(typoid, atttypmod) + END AS base_type + , pk.conkey AS pk_columns + , ARRAY[attnum] <@ pk.conkey AS is_pk_member + , (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) + FROM pg_catalog.pg_attrdef d + WHERE d.adrelid = a.attrelid + AND d.adnum = a.attnum + AND a.atthasdef + ) AS column_default + FROM _cat_tools.pg_attribute_v a + LEFT JOIN pg_constraint pk + ON ( reloid = pk.conrelid ) + AND pk.contype = 'p' +; +REVOKE ALL ON _cat_tools.column FROM public; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +-- No perms on extension visibility +CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS + SELECT e.oid, e.* + + , extnamespace::regnamespace AS extschema -- SED: REQUIRES 9.5! +-- Not used prior to 9.5: , nspname AS extschema + + , extconfig::pg_catalog.regclass[] AS ext_config_tables + FROM pg_catalog.pg_extension e + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace +; +GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; + +CREATE OR REPLACE VIEW cat_tools.column AS + SELECT * + FROM _cat_tools.column + -- SECURITY + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND attnum > 0 + AND NOT attisdropped + AND relkind IN( 'r', 'v', 'f' ) + AND ( + pg_has_role(SESSION_USER, relowner, 'USAGE'::text) + OR has_column_privilege(SESSION_USER, reloid, attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY relschema, relname, attnum +; +GRANT SELECT ON cat_tools.column TO cat_tools__usage; + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +SELECT __cat_tools.create_function( + '_cat_tools._pg_sv_column_array' + , 'OID, SMALLINT[]' + , 'NAME[] LANGUAGE sql STABLE' + , $$ + SELECT ARRAY( + SELECT a.attname + FROM pg_catalog.pg_attribute a + JOIN generate_series(1, array_upper($2, 1)) s(i) ON a.attnum = $2[i] + WHERE attrelid = $1 + ORDER BY i + ) +$$ +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +SELECT __cat_tools.create_function( + '_cat_tools._pg_sv_table_accessible' + , 'OID, OID' + , 'boolean LANGUAGE sql STABLE' + , $$ + SELECT CASE WHEN has_schema_privilege($1, 'USAGE') THEN ( + has_table_privilege($2, 'SELECT') + OR has_table_privilege($2, 'INSERT') + or has_table_privilege($2, 'UPDATE') + OR has_table_privilege($2, 'DELETE') + OR has_table_privilege($2, 'RULE') + OR has_table_privilege($2, 'REFERENCES') + OR has_table_privilege($2, 'TRIGGER') + ) ELSE FALSE + END; +$$ +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +CREATE OR REPLACE VIEW cat_tools.pg_all_foreign_keys +AS + SELECT n1.nspname AS fk_schema_name, + c1.relname AS fk_table_name, + k1.conname AS fk_constraint_name, + c1.oid AS fk_table_oid, + _cat_tools._pg_sv_column_array(k1.conrelid,k1.conkey) AS fk_columns, + n2.nspname AS pk_schema_name, + c2.relname AS pk_table_name, + k2.conname AS pk_constraint_name, + c2.oid AS pk_table_oid, + ci.relname AS pk_index_name, + _cat_tools._pg_sv_column_array(k1.confrelid,k1.confkey) AS pk_columns, + CASE k1.confmatchtype WHEN 'f' THEN 'FULL' + WHEN 'p' THEN 'PARTIAL' + WHEN 'u' THEN 'NONE' + else null + END AS match_type, + CASE k1.confdeltype WHEN 'a' THEN 'NO ACTION' -- -- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + else null + END AS on_delete, + CASE k1.confupdtype WHEN 'a' THEN 'NO ACTION' + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + ELSE NULL + END AS on_update, + k1.condeferrable AS is_deferrable, -- -- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + k1.condeferred AS is_deferred + FROM pg_catalog.pg_constraint k1 + JOIN pg_catalog.pg_namespace n1 ON (n1.oid = k1.connamespace) + JOIN pg_catalog.pg_class c1 ON (c1.oid = k1.conrelid) + JOIN pg_catalog.pg_class c2 ON (c2.oid = k1.confrelid) + JOIN pg_catalog.pg_namespace n2 ON (n2.oid = c2.relnamespace) + JOIN pg_catalog.pg_depend d ON ( + d.classid = 'pg_constraint'::pg_catalog.regclass -- -- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + AND d.objid = k1.oid + AND d.objsubid = 0 + AND d.deptype = 'n' + AND d.refclassid = 'pg_class'::pg_catalog.regclass + AND d.refobjsubid=0 + ) + JOIN pg_catalog.pg_class ci ON (ci.oid = d.refobjid AND ci.relkind = 'i') + LEFT JOIN pg_depend d2 ON ( + d2.classid = 'pg_class'::pg_catalog.regclass -- -- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + AND d2.objid = ci.oid + AND d2.objsubid = 0 + AND d2.deptype = 'i' + AND d2.refclassid = 'pg_constraint'::pg_catalog.regclass + AND d2.refobjsubid = 0 + ) + LEFT JOIN pg_catalog.pg_constraint k2 ON ( -- -- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + k2.oid = d2.refobjid + AND k2.contype IN ('p', 'u') + ) + WHERE k1.conrelid != 0 + AND k1.confrelid != 0 + AND k1.contype = 'f' + AND _cat_tools._pg_sv_table_accessible(n1.oid, c1.oid) +; +GRANT SELECT ON cat_tools.pg_all_foreign_keys TO cat_tools__usage; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.pg_attribute__get' + , $$ + relation pg_catalog.regclass + , column_name name +$$ + , $$pg_catalog.pg_attribute LANGUAGE plpgsql$$ + , $body$ +DECLARE + r pg_catalog.pg_attribute; +BEGIN + SELECT INTO STRICT r + * + FROM pg_catalog.pg_attribute + WHERE attrelid = relation + AND attname = column_name + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'column "%" of relation "%" does not exist', column_name, relation + USING ERRCODE = 'undefined_column' + ; +END +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.pg_extension__get' + , 'extension_name name' + , $$cat_tools.pg_extension_v LANGUAGE plpgsql$$ + , $body$ +DECLARE + r cat_tools.pg_extension_v; +BEGIN + SELECT INTO STRICT r + * + FROM cat_tools.pg_extension_v + WHERE extname = extension_name + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'extension "%" does not exist', extension_name + USING ERRCODE = 'undefined_object' + ; +END +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas' + , 'extension_names name[]' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! +-- Not used prior to 9.5: , $$pg_catalog.name[] LANGUAGE sql$$ + , $body$ +SELECT array( + SELECT (cat_tools.pg_extension__get(en)).extschema + FROM unnest(extension_names) en +) +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas_unique' + , 'extension_names name[]' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! +-- Not used prior to 9.5: , $$pg_catalog.name[] LANGUAGE sql$$ + , $body$ +SELECT array( + SELECT DISTINCT (cat_tools.pg_extension__get(en)).extschema + FROM unnest(extension_names) en +) +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +-- Text versions +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas' + , 'extension_names text' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! +-- Not used prior to 9.5: , $$pg_catalog.name[] LANGUAGE sql$$ + , $body$ +SELECT cat_tools.extension__schemas( + CASE WHEN extension_names LIKE '{%}' THEN extension_names + ELSE '{' || extension_names || '}' + END::name[] +) +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas_unique' + , 'extension_names text' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! +-- Not used prior to 9.5: , $$pg_catalog.name[] LANGUAGE sql$$ + , $body$ +SELECT cat_tools.extension__schemas_unique( + CASE WHEN extension_names LIKE '{%}' THEN extension_names + ELSE '{' || extension_names || '}' + END::name[] +) +$body$ + , 'cat_tools__usage' +); + + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.get_serial_sequence' + , $$ + table_name text + , column_name text +$$ + , $$pg_catalog.regclass LANGUAGE plpgsql$$ + , $body$ +DECLARE + seq pg_catalog.regclass; +BEGIN + -- Note: the function will throw an error if table or column doesn't exist + seq := pg_get_serial_sequence( table_name, column_name ); + + IF seq IS NULL THEN + RAISE EXCEPTION '"%" is not a serial column', column_name + USING ERRCODE = 'wrong_object_type' + -- TODO: SCHEMA and COLUMN + , COLUMN = column_name -- SED: REQUIRES 9.3! + ; + END IF; + + RETURN seq; +END +$body$ + , 'cat_tools__usage' + , 'Return sequence that is associated with a column. Unlike the pg_get_serial_sequence, throw an exception if there is no sequence associated with the column.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.sequence__last' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.currval(cat_tools.get_serial_sequence($1,$2))' + , 'cat_tools__usage' + , 'Return the last value assigned to a column with an associated sequence.' +); +SELECT __cat_tools.create_function( + 'cat_tools.currval' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.sequence__last($1,$2)' + , 'cat_tools__usage' + , 'Return the last value assigned to a column with an associated sequence.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.sequence__next' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.nextval(cat_tools.get_serial_sequence($1,$2))' + , 'cat_tools__usage' + , 'Return the next value to assign to a column with an associated sequence. THIS ADVANCES THE SEQUENCE.' +); +SELECT __cat_tools.create_function( + 'cat_tools.nextval' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.sequence__next($1,$2)' + , 'cat_tools__usage' + , 'Return the next value to assign to a column with an associated sequence. THIS ADVANCES THE SEQUENCE.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.setval' + , $$ + table_name text + , column_name text + , new_value bigint + , has_been_used boolean DEFAULT true +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.setval(cat_tools.get_serial_sequence($1,$2), $3, $4)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. If has_been_used is true, the sequence will be set to new_value + 1. Returns new_value. See also sequence__set_last() and sequence__set_next().' +); +SELECT __cat_tools.create_function( + 'cat_tools.sequence__set_last' + , $$ + table_name text + , column_name text + , last_value bigint +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.setval($1,$2,$3,true)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. last_value is the last value used, so sequence is set to last_value+1. See also sequence__set_next().' +); +SELECT __cat_tools.create_function( + 'cat_tools.sequence__set_next' + , $$ + table_name text + , column_name text + , next_value bigint +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.setval($1,$2,$3,false)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. next_value is the next value the sequence will assign. See also sequence__last_value.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.enum_range' + , 'enum pg_catalog.regtype' + , $$text[] LANGUAGE plpgsql STABLE$$ + , $body$ +DECLARE + ret text[]; +BEGIN + EXECUTE format('SELECT pg_catalog.enum_range( NULL::%s )', enum) INTO ret; + RETURN ret; +END +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.enum_range_srf' + , 'enum pg_catalog.regtype' + , $$SETOF text LANGUAGE sql$$ + , $body$ +SELECT * FROM unnest( cat_tools.enum_range($1) ) AS r(enum_label) +$body$ + , 'cat_tools__usage' +); + +SELECT __cat_tools.create_function( + 'cat_tools.pg_class' + , 'rel pg_catalog.regclass' + , $$cat_tools.pg_class_v LANGUAGE sql STABLE$$ + , $body$ +SELECT * FROM cat_tools.pg_class_v WHERE reloid = $1 +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.name__check' + , 'name_to_check text' + , $$void LANGUAGE plpgsql$$ + , $body$ +BEGIN + IF name_to_check IS DISTINCT FROM name_to_check::name THEN + RAISE '"%" becomes "%" when cast to name', name_to_check, name_to_check::name; + END IF; +END +$body$ + , 'cat_tools__usage' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +/* + * Trigger functions + */ +SELECT __cat_tools.create_function( + 'cat_tools.trigger__get_oid__loose' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text +$$ + , $$oid LANGUAGE sql$$ + , $body$ + SELECT oid + FROM pg_trigger + WHERE tgrelid = $1 --trigger_table + AND tgname = CASE + /* + * tgname isn't quoted, so strip quotes, but only if the string both + * starts and ends with quotes + */ + WHEN $2 LIKE '"%"' THEN btrim($2, '"') + ELSE $2 + END --trigger_name + ; +$body$ + , 'cat_tools__usage' + , 'Return the OID for a trigger. Returns NULL if trigger does not exist.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__get_oid' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text +$$ + , $$oid LANGUAGE plpgsql$$ + , $body$ +DECLARE + v_oid oid; +BEGIN + -- Note that because __loose isn't an SRF it'll always return a value + v_oid := cat_tools.trigger__get_oid__loose( trigger_table, trigger_name ) ; + + IF v_oid IS NULL THEN + RAISE EXCEPTION 'trigger % on table % does not exist', trigger_name, trigger_table + USING errcode = 'undefined_object' -- 42704 + ; + END IF; + + RETURN v_oid; +END +$body$ + , 'cat_tools__usage' + , 'Return the OID for a trigger. Throws an undefined_object error if the trigger does not exist.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__parse' + , $$ + trigger_oid oid + , OUT trigger_table regclass + , OUT timing text + , OUT events text[] + , OUT defer text + , OUT row_statement text + , OUT when_clause text + , OUT trigger_function regprocedure + , OUT function_arguments text[] +$$ + , $$record STABLE LANGUAGE plpgsql$$ + , $body$ +DECLARE + r_trigger pg_catalog.pg_trigger; + v_triggerdef text; + v_create_stanza text; + v_on_clause text; + v_execute_clause text; + + v_work text; + v_array text[]; +BEGIN + /* + * Do this first to make sure trigger exists. + * + * TODO: After we no longer support < 9.6, test v_triggerdef for NULL instead + * using the extra block here. + */ + BEGIN + SELECT * INTO STRICT r_trigger FROM pg_catalog.pg_trigger WHERE oid = trigger_oid; + EXCEPTION WHEN no_data_found THEN + RAISE EXCEPTION 'trigger with OID % does not exist', trigger_oid + USING errcode = 'undefined_object' -- 42704 + ; + END; + trigger_table := r_trigger.tgrelid; + trigger_function := r_trigger.tgfoid; + + v_triggerdef := pg_catalog.pg_get_triggerdef(trigger_oid, true); + + v_create_stanza := format( + 'CREATE %sTRIGGER %I ' + , CASE WHEN r_trigger.tgconstraint=0 THEN '' ELSE 'CONSTRAINT ' END + , r_trigger.tgname + ); + -- Strip CREATE [CONSTRAINT] TRIGGER ... off + v_work := replace( v_triggerdef, v_create_stanza, '' ); + + -- Get BEFORE | AFTER | INSTEAD OF + timing := split_part( v_work, ' ', 1 ); + timing := timing || CASE timing WHEN 'INSTEAD' THEN ' OF' ELSE '' END; + + -- Strip off timing clause + v_work := replace( v_work, timing || ' ', '' ); + + -- Get array of events (INSERT, UPDATE [OF column, column], DELETE, TRUNCATE) + v_on_clause := ' ON ' || r_trigger.tgrelid::pg_catalog.regclass || ' '; + v_array := regexp_split_to_array( v_work, v_on_clause ); + events := string_to_array( v_array[1], ' OR ' ); + -- Get everything after ON table_name + v_work := v_array[2]; + RAISE DEBUG 'v_work "%"', v_work; + + -- Strip off FROM referenced_table if we have it + IF r_trigger.tgconstrrelid<>0 THEN + v_work := replace( + v_work + , 'FROM ' || r_trigger.tgconstrrelid::pg_catalog.regclass || ' ' + , '' + ); + END IF; + RAISE DEBUG 'v_work "%"', v_work; + + -- Get function arguments + v_execute_clause := ' EXECUTE PROCEDURE ' || r_trigger.tgfoid::pg_catalog.regproc || E'\\('; + v_array := regexp_split_to_array( v_work, v_execute_clause ); + EXECUTE format( + 'SELECT array[ %s ]' + , rtrim( v_array[2], ')' ) -- Yank trailing ) + ) + INTO function_arguments + ; + RAISE DEBUG 'v_array[2] "%"', v_array[2]; + -- Get everything prior to EXECUTE PROCEDURE ... + v_work := v_array[1]; + RAISE DEBUG 'v_work "%"', v_work; + + row_statement := (regexp_matches( v_work, 'FOR EACH (ROW|STATEMENT)' ))[1]; + + -- Get [ NOT DEFERRABLE | [ DEFERRABLE ] { INITIALLY IMMEDIATE | INITIALLY DEFERRED } ] + v_array := regexp_split_to_array( v_work, 'FOR EACH (ROW|STATEMENT)' ); + RAISE DEBUG 'v_work = "%", v_array = "%"', v_work, v_array; + defer := rtrim(v_array[1]); + + IF r_trigger.tgqual IS NOT NULL THEN + when_clause := rtrim( + (regexp_split_to_array( v_array[2], E' WHEN \\(' ))[2] + , ')' + ); + END IF; + + RAISE DEBUG +$$v_create_stanza = "%" + v_on_clause = "%" + v_execute_clause = "%"$$ + , v_create_stanza + , v_on_clause + , v_execute_clause + ; + + RETURN; +END +$body$ + , 'cat_tools__usage' + , 'Provide details about a trigger.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__parse' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text + , OUT timing text + , OUT events text[] + , OUT defer text + , OUT row_statement text + , OUT when_clause text + , OUT trigger_function regprocedure + , OUT function_arguments text[] +$$ + , $$record STABLE LANGUAGE sql$$ + -- s/, OUT \(\w*\).*/ , \1/ + , $body$ +SELECT + timing + , events + , "defer" + , row_statement + , when_clause + , trigger_function + , function_arguments + FROM cat_tools.trigger__parse( + cat_tools.trigger__get_oid(trigger_table, trigger_name) + ) +$body$ + , 'cat_tools__usage' + , 'Provide details about a trigger.' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__args_as_text' + , $$function_arguments text[]$$ + , $$text IMMUTABLE STRICT LANGUAGE sql$$ + , $body$ + SELECT format( + $$'%s'$$ + , array_to_string( + function_arguments + , $$', '$$ + ) + ) +$body$ + , 'cat_tools__usage' + , 'Convert function_arguments as returned by trigger__parse() to text (for backwards compatibility).' +); + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +INSERT INTO _cat_tools.catalog_metadata(object_catalog, reg_type, namespace_field) +SELECT object__catalog + , CASE object__catalog + WHEN 'pg_catalog.pg_class'::pg_catalog.regclass THEN 'pg_catalog.regclass' + WHEN 'pg_catalog.pg_ts_config'::pg_catalog.regclass THEN 'pg_catalog.regconfig' + WHEN 'pg_catalog.pg_ts_dict'::pg_catalog.regclass THEN 'pg_catalog.regdictionary' + WHEN 'pg_catalog.pg_namespace'::pg_catalog.regclass THEN 'pg_catalog.regnamespace' -- SED: REQUIRES 9.5! + WHEN 'pg_catalog.pg_operator'::pg_catalog.regclass THEN 'pg_catalog.regoperator' + WHEN 'pg_catalog.pg_proc'::pg_catalog.regclass THEN 'pg_catalog.regprocedure' + WHEN 'pg_catalog.pg_authid'::pg_catalog.regclass THEN 'pg_catalog.regrole' -- SED: REQUIRES 9.5! + WHEN 'pg_catalog.pg_type'::pg_catalog.regclass THEN 'pg_catalog.regtype' + END::pg_catalog.regtype + , n.attname + FROM ( + SELECT DISTINCT cat_tools.object__catalog(object_type) + FROM cat_tools.enum_range_srf('cat_tools.object_type') r(object_type) + ) d + LEFT JOIN cat_tools.column n + ON n.attrelid = object__catalog + AND n.attname ~ 'namespace$' + AND atttypid = 'oid'::pg_catalog.regtype +; +UPDATE _cat_tools.catalog_metadata + SET simple_reg_type = 'pg_catalog.regproc' + WHERE object_catalog = 'pg_catalog.pg_proc'::pg_catalog.regclass +; +UPDATE _cat_tools.catalog_metadata + SET simple_reg_type = 'pg_catalog.regoper' + WHERE object_catalog = 'pg_catalog.pg_operator'::pg_catalog.regclass +; +-- Cluster to get rid of dead rows +CLUSTER _cat_tools.catalog_metadata USING catalog_metadata__pk_object_catalog; + +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in + +/* + * Drop "temporary" objects + */ +DROP FUNCTION __cat_tools.exec( + sql text +); +DROP FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text + , comment text +); +DROP SCHEMA __cat_tools; + +-- vi: expandtab ts=2 sw=2 +-- GENERATED FILE! DO NOT EDIT! See sql/cat_tools.sql.in diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index 60892ba..d7596b8 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -745,12 +745,15 @@ GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; @generated@ --- On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). --- We include it cast to text[] so the column is preserved; on PG10 it doesn't exist. +/* + * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). + * Always include it as text[] — NULL on PG10 (column absent), real value on PG11+. + * This ensures consistent view schema across all PG versions. + */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS SELECT %s - %s + , %s AS attmissingval , c.* , t.oid AS typoid , %s @@ -762,9 +765,11 @@ $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) , CASE WHEN EXISTS( SELECT 1 FROM pg_catalog.pg_attribute - WHERE attrelid = 'pg_catalog.pg_attribute'::regclass + WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 AND attname = 'attmissingval' - ) THEN ', a.attmissingval::text::text[]' ELSE '' END + ) THEN 'a.attmissingval::text::text[]' + ELSE 'NULL::text[]' + END , __cat_tools.omit_column('pg_catalog.pg_type') )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; @@ -796,8 +801,10 @@ REVOKE ALL ON _cat_tools.column FROM public; @generated@ --- Starting in PG12, oid became a visible column in system catalogs. --- Use omit_column to avoid duplicate oid columns. +/* + * Starting in PG12, oid became a visible column in system catalogs. + * Use omit_column to avoid duplicate oid columns. + */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS SELECT e.oid From 9ae32d84c27fed6d4953853a7e7bcca2ad20e6f6 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 7 Nov 2024 15:20:48 -0600 Subject: [PATCH 05/17] Add pgxn-tools CI --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b889baa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: [push, pull_request] +jobs: + test: + strategy: + matrix: + pg: [17, 16, 15, 14, 13, 12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2, 9.1, 9.0, 8.4, 8.3, 8.2] + name: 🐘 PostgreSQL ${{ matrix.pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + steps: + - name: Start PostgreSQL ${{ matrix.pg }} + run: pg-start ${{ matrix.pg }} + - name: Check out the repo + uses: actions/checkout@v4 + - name: Test on PostgreSQL ${{ matrix.pg }} + run: pg-build-test From 18b8ee5445f9311fb04c036d6a15fa283c082ae2 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 7 Nov 2024 15:46:32 -0600 Subject: [PATCH 06/17] Fix CI version matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b889baa..0946c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - pg: [17, 16, 15, 14, 13, 12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2, 9.1, 9.0, 8.4, 8.3, 8.2] + pg: [17, 16, 15, 14, 13, 12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2] name: 🐘 PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools From 5b4b027663dec9e7a0979ae563bed80058bf783e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 11 Nov 2025 15:47:09 -0600 Subject: [PATCH 07/17] Add automated test workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0946c32..c89c1b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - pg: [17, 16, 15, 14, 13, 12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2] + pg: [18, 17, 16, 15, 14, 13, 12, 11, 10] name: 🐘 PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -14,4 +14,4 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 - name: Test on PostgreSQL ${{ matrix.pg }} - run: pg-build-test + run: make test PGUSER=postgres From 48a9e693ff54cb7d9f2208665cedf7179da72ef6 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 7 Apr 2026 16:44:16 -0500 Subject: [PATCH 08/17] Install rsync in CI before running tests pgxntool/run-test-build.sh uses rsync to sync test/build/*.sql into test/build/sql/ for pg_regress. The pgxn/pgxn-tools container doesn't include rsync, so install it explicitly. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c89c1b2..ff64107 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,5 +13,7 @@ jobs: run: pg-start ${{ matrix.pg }} - name: Check out the repo uses: actions/checkout@v4 + - name: Install rsync + run: apt-get install -y rsync - name: Test on PostgreSQL ${{ matrix.pg }} run: make test PGUSER=postgres From 35be60b3ccbf9dd039d116680f8bc728a6aa0d4e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 11 Nov 2024 13:08:41 -0600 Subject: [PATCH 09/17] Remove .travis.yml --- .travis.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ddca483..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: c -before_install: - - wget https://gist.github.com/petere/5893799/raw/apt.postgresql.org.sh - - sudo sh ./apt.postgresql.org.sh - - sudo sh -c "echo deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs 2>/dev/null)-pgdg main $PGVERSION >> /etc/apt/sources.list.d/pgdg.list" -env: - - PGVERSION=9.6 - - PGVERSION=9.5 - - PGVERSION=9.4 - - PGVERSION=9.3 - - PGVERSION=9.2 - - - PGVERSION=9.6 TARGET='set-test-upgrade test' - - PGVERSION=9.5 TARGET='set-test-upgrade test' - - PGVERSION=9.4 TARGET='set-test-upgrade test' - - PGVERSION=9.3 TARGET='set-test-upgrade test' - - PGVERSION=9.2 TARGET='set-test-upgrade test' - -script: bash ./pg-travis-test.sh From f691b9886c82d253dd5e63609cbfb2680a9aa52d Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 14 Apr 2026 15:41:23 -0500 Subject: [PATCH 10/17] Fix test failures on PG11+/PG12+/PG15+; add CLAUDE.md coding standards - trigger__parse: handle EXECUTE FUNCTION (PG11+) and pg_temp schema alias mismatch - extension.sql test: avoid duplicate oid column (e.oid, e.*) on PG12+ - attribute.sql test: skip anyarray column (attmissingval) in results_eq comparison - object_type.sql test: grant CREATE on public schema for PG15+ compatibility - general.out: update expected output (test count was stale) - zzz_build.out: update expected output for new exec() calls - Add CLAUDE.md with SQL block comment style standard - Update .gitignore to track .claude/ but ignore *.local.json and worktrees/ Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 ++++ CLAUDE.md | 15 +++++++++++++++ sql/cat_tools.sql.in | 14 +++++++++----- test/deps.sql | 3 +++ test/expected/general.out | 3 +-- test/expected/zzz_build.out | 4 ++++ test/sql/attribute.sql | 30 ++++++++++++++++++++++++++---- test/sql/extension.sql | 6 ++++-- 8 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 3b4aafa..490e64d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ results/ regression.diffs regression.out +# Claude Code settings — track everything except local overrides +.claude/*.local.json + # Misc tmp/ .DS_Store +.claude/worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f513f57 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# Claude Code Instructions for cat_tools + +## Code Style + +### Comments +Always use block comment format for multi-line comments in SQL files: + +```sql +/* + * First line of comment. + * Second line of comment. + */ +``` + +Never use `--` line comments for multi-line explanations. diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index d7596b8..d088fc3 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -1372,12 +1372,16 @@ BEGIN RAISE DEBUG 'v_work "%"', v_work; -- Get function arguments - v_execute_clause := ' EXECUTE PROCEDURE ' || r_trigger.tgfoid::pg_catalog.regproc || E'\\('; + -- PG11+ uses "EXECUTE FUNCTION"; older versions use "EXECUTE PROCEDURE". + -- Note: ::regproc returns the internal pg_temp_N schema name while pg_get_triggerdef + -- uses the pg_temp alias, so we match on EXECUTE PROCEDURE/FUNCTION + any non-space + -- chars (the function name) rather than the specific function name. + v_execute_clause := E' EXECUTE (?:PROCEDURE|FUNCTION) \\S+\\('; v_array := regexp_split_to_array( v_work, v_execute_clause ); - EXECUTE format( - 'SELECT array[ %s ]' - , rtrim( v_array[2], ')' ) -- Yank trailing ) - ) + EXECUTE CASE + WHEN trim(rtrim(v_array[2], ')')) = '' THEN 'SELECT ARRAY[]::text[]' + ELSE format('SELECT array[ %s ]', rtrim( v_array[2], ')' )) + END INTO function_arguments ; RAISE DEBUG 'v_array[2] "%"', v_array[2]; diff --git a/test/deps.sql b/test/deps.sql index 32fd5bf..f7fe4d9 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -14,4 +14,7 @@ CREATE ROLE :no_use_role; CREATE ROLE :use_role; GRANT cat_tools__usage TO :use_role; +-- PG15+ removed CREATE on public schema from PUBLIC; grant it explicitly for tests +-- that need to create shadow names in public to test catalog lookup correctness. +GRANT CREATE ON SCHEMA public TO :use_role; diff --git a/test/expected/general.out b/test/expected/general.out index 008cc56..84e08e2 100644 --- a/test/expected/general.out +++ b/test/expected/general.out @@ -1,5 +1,4 @@ \set ECHO none -1..2 +1..1 ok 1 - Schema __cat_tools should not exist -ok 2 - verify pg_get_object_address # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/zzz_build.out b/test/expected/zzz_build.out index 754863c..dbe9615 100644 --- a/test/expected/zzz_build.out +++ b/test/expected/zzz_build.out @@ -94,6 +94,10 @@ + + + + diff --git a/test/sql/attribute.sql b/test/sql/attribute.sql index afcfe13..60ca586 100644 --- a/test/sql/attribute.sql +++ b/test/sql/attribute.sql @@ -39,6 +39,18 @@ SET LOCAL ROLE :use_role; * pg_attribute__get() */ +-- pg_attribute.attmissingval (PG11+) is anyarray pseudo-type, which has no equality +-- operator and can't be compared by pgTAP's results_eq. Build a column list that +-- excludes it so the comparison works on all PG versions. +CREATE FUNCTION pg_temp.attr_test_cols() RETURNS text LANGUAGE sql AS $$ + SELECT string_agg(attname::text, ', ' ORDER BY attnum) + FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_attribute'::regclass + AND attnum > 0 + AND NOT attisdropped + AND attname != 'attmissingval' +$$; + \set call 'SELECT * FROM %I.%I( %L, %L )' \set n pg_attribute__get SELECT throws_ok( @@ -64,20 +76,30 @@ SELECT throws_ok( SELECT results_eq( format( - :'call', :'s', :'n' + $$SELECT %s FROM %I.%I(%L, %L)$$ + , pg_temp.attr_test_cols() + , :'s', :'n' , 'pg_catalog.pg_class' , 'relname' ) - , $$SELECT * FROM pg_attribute WHERE attrelid = 'pg_class'::regclass AND attname='relname'$$ + , format( + $$SELECT %s FROM pg_attribute WHERE attrelid = 'pg_class'::regclass AND attname='relname'$$ + , pg_temp.attr_test_cols() + ) , 'Verify details of pg_class.relname' ); SELECT results_eq( format( - :'call', :'s', :'n' + $$SELECT %s FROM %I.%I(%L, %L)$$ + , pg_temp.attr_test_cols() + , :'s', :'n' , 'pg_catalog.pg_tables' , 'tablename' ) - , $$SELECT * FROM pg_attribute WHERE attrelid = 'pg_tables'::regclass AND attname='tablename'$$ + , format( + $$SELECT %s FROM pg_attribute WHERE attrelid = 'pg_tables'::regclass AND attname='tablename'$$ + , pg_temp.attr_test_cols() + ) , 'Verify details of pg_tables.tablename' ); diff --git a/test/sql/extension.sql b/test/sql/extension.sql index b79f160..c6032a5 100644 --- a/test/sql/extension.sql +++ b/test/sql/extension.sql @@ -32,12 +32,14 @@ SELECT isnt_empty( SELECT bag_eq( $$SELECT * FROM cat_tools.pg_extension__get('cat_tools')$$ , format( - $$SELECT e.oid, e.*, %s, extconfig::regclass[] AS ext_config_table + $$SELECT %s, %s, extconfig::regclass[] AS ext_config_table FROM pg_extension e JOIN pg_namespace n ON n.oid = extnamespace WHERE extname = 'cat_tools' $$ - , CASE WHEN pg_temp.major() < '905' THEN 'nspname AS extschema' + -- PG12+ includes oid in SELECT *; older versions need explicit e.oid + , CASE WHEN pg_temp.major() >= 1200 THEN 'e.*' ELSE 'e.oid, e.*' END + , CASE WHEN pg_temp.major() < 905 THEN 'nspname AS extschema' ELSE 'extnamespace::regnamespace AS extschema' END ) From e4cdd3f6ef2549704b5a3cc60400377126847b7f Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Wed, 15 Apr 2026 14:22:27 -0500 Subject: [PATCH 11/17] Makefile: drop .in pipeline; use wildcard for historical install scripts Version-specific upgrade scripts are now committed directly as .sql files rather than generated from .sql.in sources. Remove the versioned_in/versioned_out machinery and switch DATA to pick up all historical install scripts via wildcard, filtering out files already provided by pgxntool/base.mk (EXTENSION_VERSION_FILES and sql/*--*--*.sql upgrade scripts). Note: pgxs DATA is not cleaned by `make clean` (only DATA_built is), so static committed SQL files are safe to include in DATA. Also add explanatory comment to sql/.gitignore. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 17 ++++++++--------- sql/.gitignore | 3 +++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index ba0fa29..2feba82 100644 --- a/Makefile +++ b/Makefile @@ -11,15 +11,14 @@ LT93 = $(call test, $(MAJORVER), -lt, 93) $B: @mkdir -p $@ -versioned_in = $(wildcard sql/*--*--*.sql.in) -versioned_out = $(subst sql/,$B/,$(subst .sql.in,.sql,$(versioned_in))) - -all: $B/cat_tools.sql $(versioned_out) -installcheck: $B/cat_tools.sql $(versioned_out) -EXTRA_CLEAN += $B/cat_tools.sql $(versioned_out) - -# Install historical version scripts so the upgrade test can start from them -DATA += sql/cat_tools--0.2.1.sql +all: $B/cat_tools.sql +installcheck: $B/cat_tools.sql +EXTRA_CLEAN += $B/cat_tools.sql + +# Include historical install scripts (X.Y.Z only, not upgrade paths X--Y). +# Upgrade scripts (sql/*--*--*.sql) are already picked up by pgxntool/base.mk. +# pgxs DATA is not cleaned by `make clean` — only DATA_built is. +DATA += $(filter-out $(EXTENSION_VERSION_FILES) $(wildcard sql/*--*--*.sql), $(wildcard sql/*--*.sql)) # TODO: refactor the version stuff into a function # diff --git a/sql/.gitignore b/sql/.gitignore index 7c595fb..12e56ab 100644 --- a/sql/.gitignore +++ b/sql/.gitignore @@ -1 +1,4 @@ +# cat_tools.sql is generated from cat_tools.sql.in by `make` and should not be committed. +# Version-specific install/upgrade scripts (cat_tools--X.Y.Z.sql, cat_tools--X--Y.sql) +# ARE committed and tracked in git — see the parent .gitignore for details. cat_tools.sql From ea08b384a52c2f004e48abf9c931d0804a155fd2 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 13:55:55 -0500 Subject: [PATCH 12/17] Avoid DROP CASCADE in upgrade script; move attmissingval to end of view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace DROP VIEW ... CASCADE with CREATE OR REPLACE VIEW throughout the 0.2.1→0.2.2 upgrade script. On PG < 12 the column lists for pg_class_v and pg_extension_v are unchanged, so CREATE OR REPLACE VIEW works with no disruption to dependents. For pg_attribute_v, attmissingval is moved to the end of the column list (matching the new install script) so it can be added as a trailing column without invalidating any existing dependent views. Also removes the now-unnecessary recreation of pg_extension__get and the __cat_tools.create_function helper. Co-Authored-By: Claude Sonnet 4.6 --- sql/cat_tools--0.2.1--0.2.2.sql | 142 ++++---------------------------- sql/cat_tools.sql.in | 4 +- 2 files changed, 17 insertions(+), 129 deletions(-) diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql index 852d600..a06ee12 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql +++ b/sql/cat_tools--0.2.1--0.2.2.sql @@ -9,84 +9,6 @@ BEGIN END $body$; -CREATE FUNCTION __cat_tools.create_function( - function_name text - , args text - , options text - , body text - , grants text DEFAULT NULL - , comment text DEFAULT NULL -) RETURNS void LANGUAGE plpgsql AS $body$ -DECLARE - c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); - - create_template CONSTANT text := $template$ -CREATE OR REPLACE FUNCTION %s( -%s -) RETURNS %s AS -%L -$template$ - ; - - revoke_template CONSTANT text := $template$ -REVOKE ALL ON FUNCTION %s( -%s -) FROM public; -$template$ - ; - - grant_template CONSTANT text := $template$ -GRANT EXECUTE ON FUNCTION %s( -%s -) TO %s; -$template$ - ; - - comment_template CONSTANT text := $template$ -COMMENT ON FUNCTION %s( -%s -) IS %L; -$template$ - ; - -BEGIN - PERFORM __cat_tools.exec( format( - create_template - , function_name - , args - , options -- TODO: Force search_path if options ~* 'definer' - , body - ) ) - ; - PERFORM __cat_tools.exec( format( - revoke_template - , function_name - , c_simple_args - ) ) - ; - - IF grants IS NOT NULL THEN - PERFORM __cat_tools.exec( format( - grant_template - , function_name - , c_simple_args - , grants - ) ) - ; - END IF; - - IF comment IS NOT NULL THEN - PERFORM __cat_tools.exec( format( - comment_template - , function_name - , c_simple_args - , comment - ) ) - ; - END IF; -END -$body$; - CREATE FUNCTION __cat_tools.omit_column( rel text , omit name[] DEFAULT array['oid'] @@ -108,12 +30,9 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES TO cat_tools__ /* * Recreate _cat_tools.pg_class_v with dynamic column list to handle PG12+ oid visibility. - * WARNING: CASCADE will drop cat_tools.pg_class_v, _cat_tools.pg_attribute_v, - * _cat_tools.column, and cat_tools.column. Any user-defined views depending on - * cat_tools.pg_class_v or cat_tools.column must be recreated after this upgrade. + * On PG < 12, oid was already hidden so the effective column list is unchanged; + * CREATE OR REPLACE VIEW works without disturbing dependent views. */ -DROP VIEW IF EXISTS _cat_tools.pg_class_v CASCADE; - SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS SELECT c.oid AS reloid @@ -136,24 +55,25 @@ CREATE OR REPLACE VIEW cat_tools.pg_class_v AS GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; /* - * Recreate pg_attribute_v (dropped via pg_class_v CASCADE above). - * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). - * Always include it as text[] — NULL on PG10 (column absent), real value on PG11+. - * This ensures consistent view schema across all PG versions. + * Recreate _cat_tools.pg_attribute_v to handle PG11+ attmissingval. + * attmissingval is appended at the end so CREATE OR REPLACE VIEW can add it + * without invalidating dependent views — no DROP CASCADE needed. + * On PG < 11 (where attmissingval doesn't exist) it is exposed as NULL::text[]. */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS SELECT %s - , %s AS attmissingval , c.* , t.oid AS typoid , %s + , %s AS attmissingval FROM pg_attribute a LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) ; $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) + , __cat_tools.omit_column('pg_catalog.pg_type') , CASE WHEN EXISTS( SELECT 1 FROM pg_catalog.pg_attribute WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 @@ -161,11 +81,10 @@ $fmt$ ) THEN 'a.attmissingval::text::text[]' ELSE 'NULL::text[]' END - , __cat_tools.omit_column('pg_catalog.pg_type') )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; --- Recreate _cat_tools.column (dropped by pg_class_v CASCADE above). +-- Refresh _cat_tools.column and cat_tools.column to pick up the new attmissingval column. CREATE OR REPLACE VIEW _cat_tools.column AS SELECT * , pg_catalog.format_type(typoid, atttypmod) AS column_type @@ -189,7 +108,6 @@ CREATE OR REPLACE VIEW _cat_tools.column AS ; REVOKE ALL ON _cat_tools.column FROM public; --- Recreate cat_tools.column (dropped by pg_class_v CASCADE above). CREATE OR REPLACE VIEW cat_tools.column AS SELECT * FROM _cat_tools.column @@ -205,9 +123,11 @@ CREATE OR REPLACE VIEW cat_tools.column AS ; GRANT SELECT ON cat_tools.column TO cat_tools__usage; --- Fix cat_tools.pg_extension_v for PG12+ oid visibility. --- CASCADE is required: pg_extension__get(name) depends on pg_extension_v's row type. -DROP VIEW IF EXISTS cat_tools.pg_extension_v CASCADE; +/* + * Fix cat_tools.pg_extension_v for PG12+ oid visibility. + * On PG < 12, oid was already hidden so the effective column list is unchanged; + * CREATE OR REPLACE VIEW works without disturbing pg_extension__get or other dependents. + */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS SELECT e.oid @@ -222,44 +142,12 @@ $fmt$ )); GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; --- Recreate cat_tools.pg_extension__get (dropped by the CASCADE above). -SELECT __cat_tools.create_function( - 'cat_tools.pg_extension__get' - , 'extension_name name' - , $$cat_tools.pg_extension_v LANGUAGE plpgsql$$ - , $body$ -DECLARE - r cat_tools.pg_extension_v; -BEGIN - SELECT INTO STRICT r - * - FROM cat_tools.pg_extension_v - WHERE extname = extension_name - ; - RETURN r; -EXCEPTION WHEN no_data_found THEN - RAISE 'extension "%" does not exist', extension_name - USING ERRCODE = 'undefined_object' - ; -END -$body$ - , 'cat_tools__usage' -); - -- Drop temporary helper objects DROP FUNCTION __cat_tools.omit_column( rel text - , omit name[] -- DEFAULT array['oid'] + , omit name[] ); DROP FUNCTION __cat_tools.exec( sql text ); -DROP FUNCTION __cat_tools.create_function( - function_name text - , args text - , options text - , body text - , grants text - , comment text -); DROP SCHEMA __cat_tools; diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index d088fc3..f1ba735 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -753,16 +753,17 @@ GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS SELECT %s - , %s AS attmissingval , c.* , t.oid AS typoid , %s + , %s AS attmissingval FROM pg_attribute a LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) ; $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) + , __cat_tools.omit_column('pg_catalog.pg_type') , CASE WHEN EXISTS( SELECT 1 FROM pg_catalog.pg_attribute WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 @@ -770,7 +771,6 @@ $fmt$ ) THEN 'a.attmissingval::text::text[]' ELSE 'NULL::text[]' END - , __cat_tools.omit_column('pg_catalog.pg_type') )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; From 3c45da2e02251783d626ba6ee9d4461843226f77 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 14:13:57 -0500 Subject: [PATCH 13/17] Remove redundant include control.mk; base.mk already includes it pgxntool 2.0 added -include control.mk to base.mk. Having it again in the Makefile caused control.mk to be processed twice, doubling up EXTENSION_VERSION_FILES (duplicate sql/cat_tools--0.2.2.sql in DATA) and triggering "overriding recipe" warnings. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 83d9d97..6fe257a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ B = sql testdeps: $(wildcard test/*.sql test/helpers/*.sql) # Be careful not to include directories in this include pgxntool/base.mk -include control.mk LT95 = $(call test, $(MAJORVER), -lt, 95) LT93 = $(call test, $(MAJORVER), -lt, 93) From ec3f771572bcf6a791024e0a0bf7691dd2265539 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 14:33:00 -0500 Subject: [PATCH 14/17] Fix upgrade script: runtime detection for extschema on PG < 9.5 ::regnamespace was hardcoded but that type only exists on PG 9.5+. The upgrade script is pre-built SQL (no SED processing), so use the same runtime EXISTS check pattern as the attmissingval fix. Co-Authored-By: Claude Sonnet 4.6 --- sql/cat_tools--0.2.1--0.2.2.sql | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql index a06ee12..d6e286d 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql +++ b/sql/cat_tools--0.2.1--0.2.2.sql @@ -132,13 +132,20 @@ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS SELECT e.oid , %s - , extnamespace::regnamespace AS extschema + , %s AS extschema , extconfig::pg_catalog.regclass[] AS ext_config_tables FROM pg_catalog.pg_extension e LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace ; $fmt$ , __cat_tools.omit_column('pg_catalog.pg_extension') + , CASE WHEN EXISTS( + SELECT 1 FROM pg_catalog.pg_type + WHERE typname = 'regnamespace' + AND typnamespace = 11 -- pg_catalog + ) THEN 'extnamespace::regnamespace' + ELSE 'nspname' + END )); GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; From 3363ffe2abdf7e481f4a37f71df35655952737af Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 14:41:14 -0500 Subject: [PATCH 15/17] Convert upgrade script to .sql.in with SED markers for extschema The upgrade script is now SED-processed like the install scripts. Replaces the runtime EXISTS/regnamespace check with proper -- SED: REQUIRES 9.5! / -- SED: PRIOR TO 9.5! markers. Co-Authored-By: Claude Sonnet 4.6 --- sql/.gitignore | 1 + ...2.1--0.2.2.sql => cat_tools--0.2.1--0.2.2.sql.in} | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) rename sql/{cat_tools--0.2.1--0.2.2.sql => cat_tools--0.2.1--0.2.2.sql.in} (95%) diff --git a/sql/.gitignore b/sql/.gitignore index f567d78..1ffbd4b 100644 --- a/sql/.gitignore +++ b/sql/.gitignore @@ -7,3 +7,4 @@ cat_tools.sql # or only pre-built SQL was published to PGXN), so they are committed as-is. cat_tools--0.2.0.sql cat_tools--0.2.1.sql +cat_tools--0.2.1--0.2.2.sql diff --git a/sql/cat_tools--0.2.1--0.2.2.sql b/sql/cat_tools--0.2.1--0.2.2.sql.in similarity index 95% rename from sql/cat_tools--0.2.1--0.2.2.sql rename to sql/cat_tools--0.2.1--0.2.2.sql.in index d6e286d..3b297f7 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql +++ b/sql/cat_tools--0.2.1--0.2.2.sql.in @@ -132,20 +132,16 @@ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS SELECT e.oid , %s - , %s AS extschema + + , extnamespace::regnamespace AS extschema -- SED: REQUIRES 9.5! + , nspname AS extschema -- SED: PRIOR TO 9.5! + , extconfig::pg_catalog.regclass[] AS ext_config_tables FROM pg_catalog.pg_extension e LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace ; $fmt$ , __cat_tools.omit_column('pg_catalog.pg_extension') - , CASE WHEN EXISTS( - SELECT 1 FROM pg_catalog.pg_type - WHERE typname = 'regnamespace' - AND typnamespace = 11 -- pg_catalog - ) THEN 'extnamespace::regnamespace' - ELSE 'nspname' - END )); GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; From af7170a7b5eb52f760d48f9a3562f4bd635d1070 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 14:50:03 -0500 Subject: [PATCH 16/17] Replace runtime attmissingval detection with SED markers Add LT11 and LT12 SED variables to Makefile. Replace the CASE-in-format EXISTS check for attmissingval with -- SED: REQUIRES 11! / PRIOR TO 11! markers in both the install and upgrade scripts. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 12 ++++++++++++ sql/cat_tools--0.2.1--0.2.2.sql.in | 10 ++-------- sql/cat_tools.sql.in | 13 +++---------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 6fe257a..33d24d2 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ include pgxntool/base.mk LT95 = $(call test, $(MAJORVER), -lt, 95) LT93 = $(call test, $(MAJORVER), -lt, 93) +LT12 = $(call test, $(MAJORVER), -lt, 120) +LT11 = $(call test, $(MAJORVER), -lt, 110) $B: @mkdir -p $@ @@ -44,6 +46,16 @@ ifeq ($(LT93),yes) pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: REQUIRES 9.3!/-- Requires 9.3: \1/' else pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: PRIOR TO 9.3!/-- Not used prior to 9.3: \1/' +endif +ifeq ($(LT12),yes) + pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: REQUIRES 12!/-- Requires 12: \1/' +else + pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: PRIOR TO 12!/-- Not used prior to 12: \1/' +endif +ifeq ($(LT11),yes) + pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: REQUIRES 11!/-- Requires 11: \1/' +else + pgxntool/safesed $@.tmp -E -e 's/(.*)-- SED: PRIOR TO 11!/-- Not used prior to 11: \1/' endif mv $@.tmp $@ diff --git a/sql/cat_tools--0.2.1--0.2.2.sql.in b/sql/cat_tools--0.2.1--0.2.2.sql.in index 3b297f7..c11d4b4 100644 --- a/sql/cat_tools--0.2.1--0.2.2.sql.in +++ b/sql/cat_tools--0.2.1--0.2.2.sql.in @@ -66,7 +66,8 @@ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS , c.* , t.oid AS typoid , %s - , %s AS attmissingval + , a.attmissingval::text::text[] AS attmissingval -- SED: REQUIRES 11! + , NULL::text[] AS attmissingval -- SED: PRIOR TO 11! FROM pg_attribute a LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) @@ -74,13 +75,6 @@ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) , __cat_tools.omit_column('pg_catalog.pg_type') - , CASE WHEN EXISTS( - SELECT 1 FROM pg_catalog.pg_attribute - WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 - AND attname = 'attmissingval' - ) THEN 'a.attmissingval::text::text[]' - ELSE 'NULL::text[]' - END )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index f1ba735..aac0127 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -747,8 +747,7 @@ GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; /* * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). - * Always include it as text[] — NULL on PG10 (column absent), real value on PG11+. - * This ensures consistent view schema across all PG versions. + * Include it cast to text[] on PG11+; expose as NULL::text[] on older versions. */ SELECT __cat_tools.exec(format($fmt$ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS @@ -756,7 +755,8 @@ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS , c.* , t.oid AS typoid , %s - , %s AS attmissingval + , a.attmissingval::text::text[] AS attmissingval -- SED: REQUIRES 11! + , NULL::text[] AS attmissingval -- SED: PRIOR TO 11! FROM pg_attribute a LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) @@ -764,13 +764,6 @@ CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS $fmt$ , __cat_tools.omit_column('pg_catalog.pg_attribute', array['attmissingval']) , __cat_tools.omit_column('pg_catalog.pg_type') - , CASE WHEN EXISTS( - SELECT 1 FROM pg_catalog.pg_attribute - WHERE attrelid = 1249 -- OID of pg_catalog.pg_attribute, always 1249 - AND attname = 'attmissingval' - ) THEN 'a.attmissingval::text::text[]' - ELSE 'NULL::text[]' - END )); REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; From 1d58ff6c3d4b662caab887a1726628ed1a68470e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Apr 2026 15:01:33 -0500 Subject: [PATCH 17/17] Fix pre-processed SED marker in cat_tools.sql.in Restore -- SED: PRIOR TO 9.5! on nspname line; it had been left in already-processed form (-- Not used prior to 9.5:). Co-Authored-By: Claude Sonnet 4.6 --- sql/cat_tools.sql.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index aac0127..0db7d14 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -804,7 +804,7 @@ CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS , %s , extnamespace::regnamespace AS extschema -- SED: REQUIRES 9.5! --- Not used prior to 9.5: , nspname AS extschema + , nspname AS extschema -- SED: PRIOR TO 9.5! , extconfig::pg_catalog.regclass[] AS ext_config_tables FROM pg_catalog.pg_extension e