-- pg_trickle 0.87.9 -> 0.87.10 upgrade migration -- LSEC-10..LSEC-12: complete owner policy, atomic bulk lifecycle, and -- fail-closed upgrade preflight. -- -- The preflight runs before any ALTER FUNCTION or CREATE FUNCTION statement. -- It is read-only; an exception aborts the extension update transaction and -- leaves the installed catalog unchanged until the listed grants are fixed. DO $$ DECLARE missing text; BEGIN SELECT string_agg( format( 'stream table %I.%I (owner %I), source %I.%I: missing %s. Remediation: %s', stream_schema, stream_name, owner_name, source_schema, source_name, array_to_string(missing_privileges, ', '), concat( CASE WHEN missing_select THEN format('GRANT SELECT ON TABLE %I.%I TO %I; ', source_schema, source_name, owner_name) ELSE '' END, CASE WHEN missing_usage THEN format('GRANT USAGE ON SCHEMA %I TO %I;', source_schema, owner_name) ELSE '' END ) ), E'\n' ) INTO missing FROM ( SELECT st.pgt_schema AS stream_schema, st.pgt_name AS stream_name, pg_get_userbyid(storage.relowner)::text AS owner_name, source_ns.nspname::text AS source_schema, source.relname::text AS source_name, NOT has_table_privilege(pg_get_userbyid(storage.relowner), source.oid, 'SELECT') AS missing_select, NOT has_schema_privilege(pg_get_userbyid(storage.relowner), source_ns.oid, 'USAGE') AS missing_usage, ARRAY_REMOVE(ARRAY[ CASE WHEN NOT has_table_privilege(pg_get_userbyid(storage.relowner), source.oid, 'SELECT') THEN 'SELECT' END, CASE WHEN NOT has_schema_privilege(pg_get_userbyid(storage.relowner), source_ns.oid, 'USAGE') THEN 'USAGE ON SCHEMA' END ], NULL) AS missing_privileges FROM pgtrickle.pgt_stream_tables st JOIN pg_catalog.pg_class storage ON storage.oid = st.pgt_relid JOIN pgtrickle.pgt_dependencies dep ON dep.pgt_id = st.pgt_id JOIN pg_catalog.pg_class source ON source.oid = dep.source_relid JOIN pg_catalog.pg_namespace source_ns ON source_ns.oid = source.relnamespace WHERE dep.source_type IN ('TABLE', 'STREAM_TABLE', 'VIEW', 'FOREIGN_TABLE', 'MATVIEW') ) gaps WHERE missing_select OR missing_usage; IF missing IS NOT NULL THEN RAISE EXCEPTION 'pg_trickle 0.87.10 upgrade preflight failed; fix these owner privileges before retrying:%', E'\n' || missing USING HINT = 'Apply the exact GRANT statements in the remediation text and rerun ALTER EXTENSION pg_trickle UPDATE.'; END IF; END $$; -- ALTER FUNCTION preserves existing ACLs. The Rust entry points capture the -- outer caller and check the canonical stream-table owner before mutation. ALTER FUNCTION pgtrickle.bulk_alter_stream_tables(text[], json) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.bulk_alter_stream_tables(text[], json) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.bulk_drop_stream_tables(text[]) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.bulk_drop_stream_tables(text[]) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.pause_stream_table(text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.pause_stream_table(text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.refresh_stream_table(text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.refresh_stream_table(text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.repair_stream_table(text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.repair_stream_table(text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.reset_fuse(text, text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.reset_fuse(text, text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.resume_stream_table(text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.resume_stream_table(text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.set_stream_table_refresh_policy(text, text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.set_stream_table_refresh_policy(text, text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.set_stream_table_storage_policy(text, boolean, text) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.set_stream_table_storage_policy(text, boolean, text) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.stat_reset(bigint) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.stat_reset(bigint) SET search_path = pgtrickle, pg_catalog, pg_temp; ALTER FUNCTION pgtrickle.set_stream_table_sla(text, interval) SECURITY DEFINER; -- nosemgrep: sql.security-definer.present — search_path is pinned by the following ALTER FUNCTION. ALTER FUNCTION pgtrickle.set_stream_table_sla(text, interval) SET search_path = pgtrickle, pg_catalog, pg_temp; -- Added in 0.87.10; fresh installs get this from the generated archive. CREATE FUNCTION pgtrickle."lifecycle_preflight"() RETURNS text STRICT LANGUAGE c AS 'MODULE_PATHNAME', 'lifecycle_preflight_wrapper'; REVOKE EXECUTE ON FUNCTION pgtrickle.lifecycle_preflight() FROM PUBLIC;