diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9fb30d8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI +on: [push, pull_request] +jobs: + test: + strategy: + matrix: + pg: [17, 16, 15, 14, 13, 12, 11, 10] + 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: make test PGUSER=postgres + continue-on-error: true + - name: Output failed results + run: | + if [ -e test/regression.out ]; then + ls -la /var/log + ls -la /var/log/postgresql/ + cat /var/log/postgresql/postgresql-${{ matrix.pg }}-test.log + exit 1 + fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c4e016..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +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 - -script: bash ./pg-travis-test.sh diff --git a/META.in.json b/META.in.json index d01dc70..0e6db37 100644 --- a/META.in.json +++ b/META.in.json @@ -9,14 +9,14 @@ "X_comment": "The pgxntool Makefile will strip out empty keys to produce a working META.json, so it's fine to leave them here.", "X_comment": "NOTE: Don't get too cute with reformatting this file... some of the processing against it doesn't use a full JSON parser", "X_comment": "", - "X_comment": "I've tried to put things in a logical order. Comments welcome at https://github.com/decibel/pgxntool/issues!", + "X_comment": "I've tried to put things in a logical order. Comments welcome at https://github.com/Postgres-Extensions/pgxntool/issues!", "X_comment": "REQUIRED. Name of distribution.", "X_comment": "Note! Distribution names currently can't contain spaces.", "name": "object_reference", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.1.0", + "version": "0.2.0", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Provides immutable references to Postgres objects.", @@ -41,7 +41,7 @@ "file": "sql/object_reference.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.1.0", + "version": "0.2.0", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Provides immutable references to Postgres objects.", @@ -58,13 +58,13 @@ "X_comment": "Optional. \"resources\": Web resources available for this distribution. http://pgxn.org/spec/#resources", "resources": { - "homepage": "http://github.com/decibel/object_reference/", + "homepage": "http://github.com/Postgres-Extensions/object_reference/", "bugtracker": { - "web": "http://github.com/decibel/object_reference/issues" + "web": "http://github.com/Postgres-Extensions/object_reference/issues" }, "repository": { - "url": "git://github.com/decibel/object_reference.git", - "web": "http://github.com/decibel/object_reference/", + "url": "git://github.com/Postgres-Extensions/object_reference.git", + "web": "http://github.com/Postgres-Extensions/object_reference/", "type": "git" } }, @@ -76,11 +76,12 @@ "build": { "requires": { - "PostgreSQL": "9.5.0" + "PostgreSQL": "13.0" } }, "runtime": { "requires": { + "cat_tools": 0, "plpgsql": 0 } }, @@ -95,7 +96,7 @@ "no_index": "", "X_comment": "Unusual. Package/tool used to generate this file.", - "generated_by": "pgxntool https://github.com/decibel/pgxntool", + "generated_by": "pgxntool https://github.com/Postgres-Extensions/pgxntool", "X_comment": "REQUIRED. Version info for this file. http://pgxn.org/spec/#meta-spec", "meta-spec": { diff --git a/META.json b/META.json index 0512fbb..a01a9fc 100644 --- a/META.json +++ b/META.json @@ -9,14 +9,14 @@ "X_comment": "Keys marked REQUIRED or Optional are what you'd expect. Keys marked Unusual are keys you don't normally need to use.", "X_comment": "The pgxntool Makefile will strip out empty keys to produce a working META.json, so it's fine to leave them here.", "X_comment": "NOTE: Don't get too cute with reformatting this file... some of the processing against it doesn't use a full JSON parser", - "X_comment": "I've tried to put things in a logical order. Comments welcome at https://github.com/decibel/pgxntool/issues!", + "X_comment": "I've tried to put things in a logical order. Comments welcome at https://github.com/Postgres-Extensions/pgxntool/issues!", "X_comment": "REQUIRED. Name of distribution.", "X_comment": "Note! Distribution names currently can't contain spaces.", "name": "object_reference", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.1.0", + "version": "0.2.0", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Provides immutable references to Postgres objects.", @@ -40,7 +40,7 @@ "file": "sql/object_reference.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.1.0", + "version": "0.2.0", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Provides immutable references to Postgres objects.", @@ -56,13 +56,13 @@ "X_comment": "Optional. \"resources\": Web resources available for this distribution. http://pgxn.org/spec/#resources", "resources": { - "homepage": "http://github.com/decibel/object_reference/", + "homepage": "http://github.com/Postgres-Extensions/object_reference/", "bugtracker": { - "web": "http://github.com/decibel/object_reference/issues" + "web": "http://github.com/Postgres-Extensions/object_reference/issues" }, "repository": { - "url": "git://github.com/decibel/object_reference.git", - "web": "http://github.com/decibel/object_reference/", + "url": "git://github.com/Postgres-Extensions/object_reference.git", + "web": "http://github.com/Postgres-Extensions/object_reference/", "type": "git" } }, @@ -74,11 +74,12 @@ "build": { "requires": { - "PostgreSQL": "9.5.0" + "PostgreSQL": "13.0" } }, "runtime": { "requires": { + "cat_tools": 0, "plpgsql": 0 } }, @@ -92,7 +93,7 @@ "X_comment": "Unusual. \"no_index\": Files/directories that should not be indexed. http://pgxn.org/spec/#no_index", "X_comment": "Unusual. Package/tool used to generate this file.", - "generated_by": "pgxntool https://github.com/decibel/pgxntool", + "generated_by": "pgxntool https://github.com/Postgres-Extensions/pgxntool", "X_comment": "REQUIRED. Version info for this file. http://pgxn.org/spec/#meta-spec", "meta-spec": { diff --git a/Makefile b/Makefile index 9ae665d..7eec41f 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ include pgxntool/base.mk testdeps: $(wildcard test/*.sql test/helpers/*.sql) # Be careful not to include directories in this testdeps: test_factory -install: cat_tools count_nulls +install: cat_tools # pgxntool's check-stale-expected target (added in pgxntool 2.2.0) depends on # installcheck but is listed before install in TEST_DEPS, and Make evaluates a @@ -22,10 +22,6 @@ cat_tools: $(DESTDIR)$(datadir)/extension/cat_tools.control $(DESTDIR)$(datadir)/extension/cat_tools.control: pgxn install --unstable cat_tools -.PHONY: count_nulls -count_nulls: $(DESTDIR)$(datadir)/extension/count_nulls.control -$(DESTDIR)$(datadir)/extension/count_nulls.control: - pgxn install --unstable count_nulls .PHONY: test_factory test_factory: $(DESTDIR)$(datadir)/extension/test_factory.control diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bb1a6f --- /dev/null +++ b/README.md @@ -0,0 +1,333 @@ +# Object Reference Framework + +This framework provides tracking and management of references to database objects. It's designed to maintain referential integrity for objects that may be created, dropped, or renamed, and provides facilities for automatically capturing newly created objects into organized groups. + +Key capabilities: +- Track references to database objects that may be created, dropped, or renamed +- Group related objects for organization and DDL capture +- Automatically capture DDL operations to track new objects +- Manage dependencies between objects and external tables +- Support for object lifecycle management + +# A word on documentation... + +Good documentation should be like good code comments - explain things concisely without being overly verbose. Towards that end, this doc does *not* provide definition for things that should be inherently obvious, other than mentioning their existence. For example, we never define what is meant by `object_type`. The name itself should provide enough information. + +# Installation + +This extension depends on the `cat_tools` extension. + +```sql +CREATE EXTENSION object_reference CASCADE; +``` + +The extension creates two schemas: +- `object_reference` - Contains the public API functions +- `_object_reference` - Contains internal implementation details (do not use directly) + +To grant users access to the extension: +```sql +GRANT object_reference__usage TO role1, role2, role3; +``` + +# Security + +There are two roles associated with the extension: + +- `object_reference__usage` - Allows using the extension's public API functions. Grant this to users who need to track and manage object references. +- `object_reference__dependency` - Special role for creating foreign key dependencies to the internal object table. Only grant this to schemas/applications that need to create referential integrity constraints against the object tracking system. See [Referring to Objects](#referring-to-objects) below. + +Most users will only need `object_reference__usage`. The `object_reference__dependency` role is only needed when using `object__dependency__add()` or `object_group__dependency__add()` functions. + +# Key Concepts + +## Objects vs OIDs + +The framework separates object metadata (names, types, arguments) from their actual database OIDs. This allows tracking objects that don't exist yet, or that may be recreated. OID resolution is performed lazily - only when actually needed. + +## Object Groups + +Objects can be organized into named groups for logical organization. This is particularly useful for tracking all objects created during a specific operation or time period, especially when combined with DDL capture. + +## DDL Capture + +The framework can automatically capture newly created objects during DDL operations and add them to a specified object group. This is implemented using PostgreSQL event triggers. + +## Referring to Objects + +The framework supports removing objects that are no longer referenced. Because of this, *it is critical that any tables that store an `object_id` are registered with `object__dependency__add()`*. + +# API + +Note that all API routines live in the `object_reference` schema. Objects in the `_object_reference` schema are considered internal-only and should not be accessed directly. + +Most routines work with the `cat_tools.object_type` enum for specifying object types. You can also pass object types as text strings which will be converted automatically. + +## Core Object Functions + +### `object__getsert(...) RETURNS int` + +```sql +object__getsert( + object_type text | cat_tools.object_type + , object_name text + , secondary text DEFAULT NULL + , object_group_name text DEFAULT NULL + , loose boolean DEFAULT false +) RETURNS int +``` + +Get or insert an object reference, returning the `object_id`. This is the primary function for tracking objects. + +Arguments: +- `object_type` - Type of object (table, function, index, etc.) +- `object_name` - Fully qualified name of the object +- `secondary` - Additional identifier for objects that need it (e.g., function arguments) +- `object_group_name` - Optional object group to add this object to +- `loose` - If true, allows creating references to objects that don't exist + +### `object__getsert_w_group_id(...) RETURNS int` + +```sql +object__getsert_w_group_id( + object_type cat_tools.object_type + , object_name text + , secondary text DEFAULT NULL + , object_group_id int DEFAULT NULL + , loose boolean DEFAULT false +) RETURNS int +``` + +Same as `object__getsert()` but accepts a numeric `object_group_id` instead of group name. + +### `object__describe(object_id int) RETURNS text` + +Returns a human-readable description of the object, matching the format of PostgreSQL's `pg_describe_object()` function. + +### `object__identity(object_id int) RETURNS record` + +Returns object identification information matching the format of PostgreSQL's `pg_identify_object()` function. Returns a record with columns: `type`, `schema`, `name`, `identity`. + + +## Object Group Functions + +### `object_group__create(...) RETURNS int` + +```sql +object_group__create( + object_group_name text +) RETURNS int +``` + +Create a new object group and return its ID. + +### `object_group__get(...) RETURNS object_group` + +```sql +object_group__get( + object_group_name text | object_group_id int +) RETURNS _object_reference.object_group +``` + +Retrieve an object group by name or ID. Throws an error if the group doesn't exist. + +### `object_group__remove(...) RETURNS void` + +```sql +object_group__remove( + object_group_name text | object_group_id int + , force boolean DEFAULT false +) RETURNS void +``` + +Remove an object group. This does not delete the objects themselves, only the grouping. + +### `object_group__object__add(...) RETURNS void` + +```sql +object_group__object__add( + object_group_id int + , object_id int +) RETURNS void +``` + +Add an existing object to an object group. + +### `object_group__object__remove(...) RETURNS void` + +```sql +object_group__object__remove( + object_group_id int + , object_id int +) RETURNS void +``` + +Remove an object from an object group. + +## Dependency Functions + +These functions create foreign key dependencies to the object tracking system. They require the `object_reference__dependency` role. + +### `object__dependency__add(...) RETURNS void` + +```sql +object__dependency__add( + table_name text + , field_name name +) RETURNS void +``` + +Create a foreign key dependency from the specified table to the object tracking system. + +Arguments: +- `table_name` - Name of table to add dependency to +- `field_name` - Name of the field to create the foreign key on + +### `object_group__dependency__add(...) RETURNS void` + +```sql +object_group__dependency__add( + table_name text + , field_name name +) RETURNS void +``` + +Create a foreign key dependency from the specified table to the object group system. + +## DDL Capture Functions + +DDL capture allows you to automatically track objects created during DDL operations. + +### `capture__start(...) RETURNS int` + +```sql +capture__start( + object_group_name text | object_group_id int +) RETURNS int +``` + +Begin capturing newly created objects to the specified group. The group must +already exist. Returns the capture level (for nested captures). + +### `capture__stop(...) RETURNS void` + +```sql +capture__stop( + object_group_name text | object_group_id int +) RETURNS void +``` + +Stop capturing objects to the specified group. + +### `capture__get_current(...) RETURNS record` + +```sql +capture__get_current( + OUT capture_level int + , OUT object_group_id int +) RETURNS record +``` + +Get information about the current capture state. + +### `capture__get_all(...) RETURNS SETOF record` + +```sql +capture__get_all( + OUT capture_level int + , OUT object_group_id int +) RETURNS SETOF record +``` + +Get information about all active capture levels. + +## Utility Functions + +### `post_restore() RETURNS void` + +```sql +post_restore() RETURNS void +``` + +Ensures all object references are correct after a database restore. Run this after restoring from backup to fix any OID mismatches. + +### `object__cleanup(object_id int) RETURNS void` + +```sql +object__cleanup(object_id int) RETURNS void +``` + +Attempts to delete an object from the tracking system. Silently returns if the object is still referenced by other tables (via foreign keys). This function is automatically called when objects are removed from object groups. + +### Object Type Information Functions + +**Get lists of unsupported/untested object types:** + +```sql +unsupported() RETURNS cat_tools.object_type[] +unsupported_srf() RETURNS SETOF cat_tools.object_type + +untested() RETURNS cat_tools.object_type[] +untested_srf() RETURNS SETOF cat_tools.object_type +``` + +**Check if specific object types are supported/tested:** + +```sql +unsupported(object_type text | cat_tools.object_type) RETURNS boolean +untested(object_type text | cat_tools.object_type) RETURNS boolean +``` + +These functions help determine which object types are supported by the framework. Unsupported types cannot be tracked, while untested types may work but haven't been fully validated. + +# Event Triggers + +The extension automatically installs several event triggers that: + +- Capture object creation when DDL capture is active +- Update object identity information when objects are renamed +- Clean up object references when objects are dropped + +These event triggers operate transparently and require no user intervention. However, be aware that they may add slight overhead to DDL operations. + +# Examples + +## Basic Object Tracking + +```sql +-- Track a table +SELECT object_reference.object__getsert('table', 'public.my_table'); + +-- Track a function with its signature +SELECT object_reference.object__getsert('function', 'public.my_func', 'integer, text'); +``` + +## Using Object Groups + +```sql +-- Create a group for related objects +SELECT object_reference.object_group__create('my_feature_objects'); + +-- Add objects to the group +SELECT object_reference.object__getsert('table', 'public.feature_table', NULL, 'my_feature_objects'); +SELECT object_reference.object__getsert('view', 'public.feature_view', NULL, 'my_feature_objects'); +``` + +## DDL Capture + +```sql +-- Create a group first +SELECT object_reference.object_group__create('migration_v2_objects'); + +-- Start capturing new objects to that group +SELECT object_reference.capture__start('migration_v2_objects'); + +-- Run your DDL commands +CREATE TABLE public.new_table (id int, name text); +CREATE INDEX idx_new_table_name ON public.new_table (name); + +-- Stop capturing +SELECT object_reference.capture__stop('migration_v2_objects'); + +-- All objects created between start/stop are now tracked in the 'migration_v2_objects' group +``` \ No newline at end of file diff --git a/object_reference.control b/object_reference.control index 8200390..3ac269c 100644 --- a/object_reference.control +++ b/object_reference.control @@ -1,5 +1,5 @@ comment = 'Provides reference IDs for database objects' -default_version = '0.1.0' +default_version = '0.2.0' relocatable = false schema = 'object_reference' requires = 'cat_tools, count_nulls' diff --git a/pg-travis-test.sh b/pg-travis-test.sh deleted file mode 100644 index f63ae43..0000000 --- a/pg-travis-test.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Based on https://gist.github.com/petere/6023944 - -set -eux - -sudo apt-get update - -packages="python-setuptools postgresql-$PGVERSION postgresql-server-dev-$PGVERSION postgresql-common" - -# bug: http://www.postgresql.org/message-id/20130508192711.GA9243@msgid.df7cb.de -sudo update-alternatives --remove-all postmaster.1.gz - -# stop all existing instances (because of https://github.com/travis-ci/travis-cookbooks/pull/221) -sudo service postgresql stop -# and make sure they don't come back -echo 'exit 0' | sudo tee /etc/init.d/postgresql -sudo chmod a+x /etc/init.d/postgresql - -sudo apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install $packages - -sudo easy_install pgxnclient - -PGPORT=55435 -PGCLUSTER_NAME=test - -export PGCLUSTER=9.6/$PGCLUSTER_NAME -env | grep PG -which pg_dump - -sudo pg_createcluster --start $PGVERSION $PGCLUSTER_NAME -p $PGPORT -- -A trust - -# TODO: have base.mk support dynamic sudo -sudo PGPORT=$PGPORT PGUSER=postgres PG_CONFIG=/usr/lib/postgresql/$PGVERSION/bin/pg_config make test - -[ ! -e test/regression.diffs ] diff --git a/sql/.object_reference.sql.swo b/sql/.object_reference.sql.swo deleted file mode 100644 index b3912fe..0000000 Binary files a/sql/.object_reference.sql.swo and /dev/null differ diff --git a/sql/object_reference--0.2.0.sql b/sql/object_reference--0.2.0.sql new file mode 100644 index 0000000..f09d345 --- /dev/null +++ b/sql/object_reference--0.2.0.sql @@ -0,0 +1,1596 @@ +/* DO NOT EDIT - AUTO-GENERATED FILE */ +\echo This extension must be loaded via 'CREATE EXTENSION object_reference;' +\echo You really, REALLY do NOT want to try and load this via psql!!! +\echo It will FAIL during pg_dump! \quit + +DO $$ +BEGIN + CREATE ROLE object_reference__usage NOLOGIN; +EXCEPTION WHEN duplicate_object THEN + NULL; +END +$$; + +DO $$ +BEGIN + CREATE ROLE object_reference__dependency 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 __object_reference; + +CREATE FUNCTION __object_reference.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE FUNCTION __object_reference.safe_dump( + relation regclass + , filter text DEFAULT '' +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + PERFORM pg_catalog.pg_extension_config_dump(relation, filter); +EXCEPTION WHEN feature_not_supported THEN + RAISE WARNING 'I promise you will be sorry if you try to use this as anything other than an extension!'; +END +$body$; + +CREATE FUNCTION __object_reference.create_function( + function_name text + , args text + , options text + , body text + , comment text + , grants text DEFAULT NULL +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_clean_args text := cat_tools.routine__parse_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 __object_reference.exec( format( + create_template + , function_name + , args + , options -- TODO: Force search_path if options ~* 'definer' + , body + ) ) + ; + PERFORM __object_reference.exec( format( + revoke_template + , function_name + , c_clean_args + ) ) + ; + + IF grants IS NOT NULL THEN + PERFORM __object_reference.exec( format( + grant_template + , function_name + , c_clean_args + , grants + ) ) + ; + END IF; + + IF comment IS NOT NULL THEN + PERFORM __object_reference.exec( format( + comment_template + , function_name + , c_clean_args + , comment + ) ) + ; + END IF; +END +$body$; + + +-- Schema already created via CREATE EXTENSION +GRANT USAGE ON SCHEMA object_reference TO object_reference__usage; +CREATE SCHEMA _object_reference; +GRANT USAGE ON SCHEMA _object_reference TO object_reference__dependency; + +SELECT __object_reference.create_function( + '_object_reference.exec' + , 'sql text' + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$ + , 'Execute arbitrary SQL with logging.' +); + +CREATE TABLE _object_reference.object( + object_id serial PRIMARY KEY + , object_type cat_tools.object_type NOT NULL +-- , original_name text NOT NULL + , object_names text[] NOT NULL + , object_args text[] NOT NULL + , CONSTRAINT object__u_object_names__object_args UNIQUE( object_type, object_names, object_args ) + /* TODO: this can't be a trigger because some objects won't exist when a dump is loaded + , CONSTRAINT object__address_sanity + -- pg_get_object_address will throw an error if anything is wrong, so the IS NOT NULL is mostly pointless + CHECK( pg_catalog.pg_get_object_address(object_type::text, object_names, object_args) IS NOT NULL ) + */ +); +SELECT __object_reference.safe_dump('_object_reference.object'); +SELECT __object_reference.safe_dump('_object_reference.object_object_id_seq'); +GRANT REFERENCES ON _object_reference.object TO object_reference__dependency; + +CREATE TABLE _object_reference._object_oid( + object_id int PRIMARY KEY REFERENCES _object_reference.object ON DELETE CASCADE ON UPDATE CASCADE + , classid oid NOT NULL + /* TODO: needs to be a trigger + CONSTRAINT classid_must_match__object__address_classid + CHECK( classid IS NOT DISTINCT FROM cat_tools.object__address_classid(object_type) ) + */ + , objid oid NOT NULL + , objsubid int NOT NULL + CONSTRAINT objid_must_match CHECK( -- _object_reference._sanity() depends on this! + objid IS NOT DISTINCT FROM object_oid + ) + , CONSTRAINT object__u_classid__objid__objsubid UNIQUE( classid, objid, objsubid ) + , object_oid oid NOT NULL +); + +SELECT __object_reference.create_function( + '_object_reference._sanity' + , $args$ + obj _object_reference.object + , id _object_reference._object_oid + , OUT names_ok boolean + , OUT ids_ok boolean + , OUT ids_exist boolean +$args$ + , 'RECORD LANGUAGE plpgsql STABLE' + , $body$ +DECLARE + r record; +BEGIN + ASSERT NOT obj IS NULL, 'obj may not be null'; + ASSERT id IS NULL OR obj.object_id = id.object_id, 'id must be null or object_ids must match'; + + ids_exist := NOT (id IS NULL); -- Remember this is NOT the same as id IS NOT NULL! + + BEGIN + r := pg_catalog.pg_get_object_address(obj.object_type::text, obj.object_names, obj.object_args); + names_ok := true; + + -- Assume that if get_object_address worked then the names are at least valid + ids_ok := r IS NOT DISTINCT FROM (id.classid::oid, id.objid, id.objsubid); + EXCEPTION + WHEN others THEN + IF + SQLSTATE IN( + '22023' -- invalid_parameter_value + , '3F000' -- invalid_schema_name + , '42703' -- undefined_column + , '42704' -- undefined_object + , '42883' -- undefined_function + ) + OR SQLSTATE LIKE '42P%' -- Matches a bunch of codes, including undefined_* and invalid_*_definition + THEN + names_ok := false; + ids_ok := false; -- Should we see if pg_object_identity_as_address works?? + ELSE + RAISE WARNING 'Unexpected error!!'; + RAISE; -- Unexpected, so re-raise + END IF; + END; +END +$body$ + , 'Check the sanity of object and _object_oid' +); + +CREATE VIEW _object_reference._object_v AS + SELECT + o.object_id + , o.object_type + , o.object_names + , o.object_args + , i.classid + , i.objid + , i.objsubid + , i.object_oid + , s.* + FROM _object_reference.object o + LEFT JOIN _object_reference._object_oid i USING(object_id) + , _object_reference._sanity(o, i) s +; +CREATE VIEW _object_reference._object_v__for_update AS + SELECT + o.object_id + , o.object_type + , o.object_names + , o.object_args + , i.classid + , i.objid + , i.objsubid + , i.object_oid + , s.* + FROM _object_reference.object o + LEFT JOIN _object_reference._object_oid i USING(object_id) + , _object_reference._sanity(o, i) s + FOR UPDATE OF o +; + +SELECT __object_reference.create_function( + '_object_reference._object_oid__add' + , $args$ + object_id _object_reference._object_oid.object_id%TYPE + , object_type _object_reference.object.object_type%TYPE DEFAULT NULL + , classid _object_reference._object_oid.classid%TYPE DEFAULT NULL + , objid _object_reference._object_oid.objid%TYPE DEFAULT NULL + , objsubid _object_reference._object_oid.objsubid%TYPE DEFAULT NULL +$args$ + , '_object_reference._object_v LANGUAGE plpgsql' + , $body$ +DECLARE + r_object_v _object_reference._object_v; +BEGIN + IF object_type IS NULL THEN + -- Should definitely exist + SELECT INTO STRICT object_type, classid, objid, objsubid + o.object_type, a.classid, a.objid, a.objsubid + FROM _object_reference.object o + , pg_catalog.pg_get_object_address(o.object_type::text, o.object_names, o.object_args) a + WHERE o.object_id = _object_oid__add.object_id + ; + END IF; + BEGIN + INSERT INTO _object_reference._object_oid(object_id, classid, objid, objsubid, object_oid) + VALUES (object_id, classid, objid, objsubid, objid); + + SELECT INTO STRICT r_object_v -- Record better exist! + * + FROM _object_reference._object_v__for_update o + WHERE o.object_id = _object_oid__add.object_id + ; + END; + + IF NOT r_object_v.ids_ok THEN + RAISE 'id mismatch for object_id %', object_id + USING + DETAIL = '_object_reference._object_v = ' || pg_catalog.row_to_json(r_object_v) + , HINT = 'this should not be possible' + ; + END IF; + + RETURN r_object_v; +END +$body$ + , 'Check the sanity of object and _object_oid' +); + +/* + * fix_refs / post_restore + */ +SELECT __object_reference.create_function( + '_object_reference.fix_refs' + , 'warning_only boolean' + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + r_object_v _object_reference._object_v; +BEGIN + FOR r_object_v IN + SELECT * FROM _object_reference._object_v + LOOP + CASE + WHEN r_object_v.names_ok AND r_object_v.ids_ok THEN + NULL; -- All good! + WHEN NOT r_object_v.names_ok THEN + IF r_object_v.ids_exist THEN + -- Only happens if things are out of sync, so intentionally treat this as an error + RAISE 'names/args are out of sync on object_id %', r_object_v.object_id + USING + DETAIL = '_object_reference._object_v = ' || pg_catalog.row_to_json(r_object_v) + , HINT = CASE WHEN r_object_v.ids_ok THEN + E'The IDs are OK though. This should not happen, but may be fixable.\n' + || 'Sanity-check the record and if OK then UPDATE _object_identity.object.' + ELSE + 'There is also a record in _object_identity._object_oid with invalid IDs. This should never happen.' + END + ; + ELSE + IF warning_only THEN + RAISE WARNING 'names not ok for object_id %', r_object_v.object_id + USING DETAIL = format( + 'pg_catalog.pg_get_object_address(%L, %L, %L)' + , r_object_v.object_type + , r_object_v.object_names + , r_object_v.object_args + ) + ; + ELSE + RAISE 'names not ok for object_id %', r_object_v.object_id + USING DETAIL = format( + 'pg_catalog.pg_get_object_address(%L, %L, %L)' + , r_object_v.object_type + , r_object_v.object_names + , r_object_v.object_args + ) + ; + END IF; + END IF; + + -- at this point, names are OK but ids are not (or don't exist) + WHEN NOT r_object_v.ids_exist THEN + IF warning_only THEN + -- This is a normal condition during a restore, so just fix it + PERFORM _object_reference._object_oid__add(r_object_v.object_id); + ELSE + RAISE 'no record in _object_reference._object_oid for object_id %', r_object_v.object_id + USING + DETAIL = '_object_reference._object_v = ' || pg_catalog.row_to_json(r_object_v) + , HINT = 'It should be safe to fix this by calling _object_reference.fix_refs()' + ; + END IF; + WHEN r_object_v.ids_exist THEN + IF warning_only THEN + RAISE WARNING 'extraneous ID information for object_id %', r_object.object_id + USING + DETAIL = '_object_reference._object_v = ' || pg_catalog.row_to_json(r_object_v) + , HINT = E'The names are OK though. This should not happen, but may be fixable.\n' + || 'Sanity-check the record and if OK then UPDATE _object_identity._object_v.' + ; + ELSE + RAISE 'extraneous ID information for object_id %', r_object.object_id + USING + DETAIL = '_object_reference._object_v = ' || pg_catalog.row_to_json(r_object_v) + , HINT = E'The names are OK though. This should not happen, but may be fixable.\n' + || 'Sanity-check the record and if OK then UPDATE _object_identity._object_v.' + ; + END IF; + END CASE; + END LOOP; +END +$body$ + , 'Fixes records in _object_reference._object_oid after a restore.' +); +SELECT __object_reference.create_function( + 'object_reference.post_restore' + , '' + , 'void SECURITY DEFINER LANGUAGE sql' + , 'SELECT _object_reference.fix_refs(false)' + , 'Ensures all object references are correct after a restore.' + , 'object_reference__usage' +); + + +SELECT __object_reference.create_function( + '_object_reference._repair' + , '' + , 'bigint SECURITY DEFINER LANGUAGE sql' + , 'SELECT count(*) AS objects FROM _object_reference.object, _object_reference._object_oid__add(object_id)' + , 'Ensures all object references are correct after a restore.' + , 'object_reference__usage' +); + +CREATE MATERIALIZED VIEW _object_reference._sentry_mv AS SELECT _object_reference._repair(); +SELECT __object_reference.safe_dump('_object_reference._sentry_mv'); + +/* + * Unsupported object types + */ +SELECT __object_reference.create_function( + 'object_reference.unsupported' + , '' + , 'cat_tools.object_type[] LANGUAGE sql IMMUTABLE' + , $body$ +SELECT cat_tools.objects__shared() + || cat_tools.objects__address_unsupported() + || '{event trigger, partitioned table, partitioned index}' +$body$ + , 'Returns array of object types that are not supported.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.unsupported_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql IMMUTABLE' + , $body$ +SELECT * FROM unnest(object_reference.unsupported()) +$body$ + , 'Returns set of object types that are not supported.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.unsupported' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql IMMUTABLE' + , $body$ +SELECT object_type = ANY(object_reference.unsupported()) +$body$ + , 'Is a object_type unsupported?' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.unsupported' + , 'object_type text' + , 'boolean LANGUAGE sql IMMUTABLE' + , $body$ +SELECT object_reference.unsupported(object_type::cat_tools.object_type) +$body$ + , 'Is a object_type unsupported?' + , 'object_reference__usage' +); + +/* + * Untested object types + */ +SELECT __object_reference.create_function( + 'object_reference.untested' + , '' + , 'cat_tools.object_type[] LANGUAGE sql IMMUTABLE' + , $body$ +SELECT '{ +foreign table, foreign table column, aggregate, collation, conversion, language, +large object, operator, operator class, operator family, operator of access method, +function of access method, rule, text search parser, text search dictionary, +text search template, text search configuration, foreign-data wrapper, server, +user mapping, default acl, transform, access method, extension, policy +}'::cat_tools.object_type[] +$body$ + , 'Returns array of object types that have not been tested.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.untested_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql IMMUTABLE' + , $body$ +SELECT * FROM unnest(object_reference.untested()) +$body$ + , 'Returns set of object types that have not been tested.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.untested' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql IMMUTABLE' + , $body$ +SELECT object_type = ANY(object_reference.untested()) +$body$ + , 'Is a object_type untested?' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.untested' + , 'object_type text' + , 'boolean LANGUAGE sql IMMUTABLE' + , $body$ +SELECT object_reference.untested(object_type::cat_tools.object_type) +$body$ + , 'Is a object_type untested?' + , 'object_reference__usage' +); + + +/* + * OBJECT GROUP + */ +CREATE TABLE _object_reference.object_group( + object_group_id serial PRIMARY KEY + , object_group_name varchar(200) NOT NULL +); +CREATE UNIQUE INDEX object_group__u_object_group_name__lower ON _object_reference.object_group(lower(object_group_name)); +SELECT __object_reference.safe_dump('_object_reference.object_group'); + +CREATE TABLE _object_reference.object_group__object( + object_group_id int NOT NULL REFERENCES _object_reference.object_group + , object_id int NOT NULL REFERENCES _object_reference.object + , CONSTRAINT object_group__object__u_object_group_id__object_id UNIQUE( object_group_id, object_id ) +); +SELECT __object_reference.safe_dump('_object_reference.object_group__object'); + +-- Trigger function for automatic object cleanup +SELECT __object_reference.create_function( + '_object_reference._object_group__object__cleanup_trigger' + , '' + , 'trigger LANGUAGE plpgsql' + , $body$ +BEGIN + PERFORM object_reference.object__cleanup(OLD.object_id); + RETURN OLD; +END +$body$ + , 'Trigger function to automatically attempt cleanup of objects when removed from groups.' +); +CREATE TRIGGER object_group__object__cleanup + AFTER DELETE ON _object_reference.object_group__object + FOR EACH ROW + EXECUTE FUNCTION _object_reference._object_group__object__cleanup_trigger(); +-- __get +SELECT __object_reference.create_function( + 'object_reference.object_group__get' + , $args$ + object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , '_object_reference.object_group LANGUAGE plpgsql STABLE' + , $body$ +DECLARE + r _object_reference.object_group; +BEGIN + SELECT INTO STRICT r + * + FROM _object_reference.object_group ogo + WHERE lower(ogo.object_group_name) = lower(object_group__get.object_group_name) + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'object group "%" does not exist', object_group_name + USING ERRCODE = 'no_data_found' + ; +END +$body$ + , 'Get details about the specified object group' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object_group__get' + , $args$ + object_group_id _object_reference.object_group.object_group_id%TYPE +$args$ + , '_object_reference.object_group LANGUAGE plpgsql STABLE' + , $body$ +DECLARE + r _object_reference.object_group; +BEGIN + SELECT INTO STRICT r + * + FROM _object_reference.object_group ogo + WHERE (ogo.object_group_id) = (object_group__get.object_group_id) + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'object group id % does not exist', object_group_id + USING ERRCODE = 'no_data_found' + ; +END +$body$ + , 'Get details about the specified object group' + , 'object_reference__usage' +); + +-- __create +SELECT __object_reference.create_function( + 'object_reference.object_group__create' + , $args$ + object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , 'int LANGUAGE sql' + , $body$ +INSERT INTO _object_reference.object_group(object_group_name) VALUES(object_group_name) + RETURNING object_group_id +$body$ + , 'Create a new object group.' + , 'object_reference__usage' +); + +-- __remove +SELECT __object_reference.create_function( + 'object_reference.object_group__remove' + , $args$ + object_group_id _object_reference.object_group.object_group_id%TYPE + , force boolean DEFAULT false +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + -- This is to ensure group exists + c_object_group_id CONSTANT int := (object_reference.object_group__get($1)).object_group_id; +BEGIN + IF force IS TRUE THEN + DELETE FROM _object_reference.object_group__object + WHERE object_group__object.object_group_id = c_object_group_id + ; + END IF; + DELETE FROM _object_reference.object_group + WHERE object_group.object_group_id = c_object_group_id + ; +END +$body$ + , 'Remove a object group. If force is true, remove group even if it still references objects.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object_group__remove' + , $args$ + object_group_name _object_reference.object_group.object_group_name%TYPE + , force boolean DEFAULT false +$args$ + , 'void LANGUAGE sql' + , $body$ +SELECT object_reference.object_group__remove( + (object_reference.object_group__get($1)).object_group_id + , $2 +); +$body$ + , 'Remove a object group. If force is true, remove group even if it still references objects.' + , 'object_reference__usage' +); + +-- __object__add +SELECT __object_reference.create_function( + 'object_reference.object_group__object__add' + , $args$ + object_group_id _object_reference.object_group__object.object_group_id%TYPE + , object_id _object_reference.object_group__object.object_id%TYPE +$args$ + , 'void LANGUAGE sql' + , $body$ + INSERT INTO _object_reference.object_group__object AS ogo(object_group_id, object_id) + VALUES($1, $2) + ON CONFLICT (object_group_id, object_id) DO NOTHING +$body$ + , 'Add a object_id to a object group.' + , 'object_reference__usage' +); + +-- __object__remove +SELECT __object_reference.create_function( + 'object_reference.object_group__object__remove' + , $args$ + object_group_id _object_reference.object_group__object.object_group_id%TYPE + , object_id _object_reference.object_group__object.object_id%TYPE +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + DELETE FROM _object_reference.object_group__object AS ogo + WHERE + ( + ogo.object_group_id + , ogo.object_id + ) = ( + -- This is to ensure group exists + (object_reference.object_group__get($1)).object_group_id + , object_group__object__remove.object_id + ) + ; + + IF NOT FOUND THEN + -- We know group exists, so issue must be that object doesn't exist + RAISE 'object id % does not exist', object_id + USING ERRCODE = 'no_data_found' + ; + END IF; +END +$body$ + , 'Remove a object_id from a object group.' + , 'object_reference__usage' +); + + +/* + * REFERENCES + */ + +SELECT __object_reference.create_function( + 'object_reference.object_group__dependency__add' + , $args$ + table_name text + , field_name name +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; +BEGIN + PERFORM _object_reference.exec( format( 'ALTER TABLE %s ADD FOREIGN KEY( %I ) REFERENCES _object_reference.object_group', table_name, field_name ) ); +END +$body$ + , 'Add a foreign key from . to the object_group table.' + , 'object_reference__dependency' +); +-- s/object_group/object/ +SELECT __object_reference.create_function( + 'object_reference.object__dependency__add' + , $args$ + table_name text + , field_name name +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; +BEGIN + PERFORM _object_reference.exec( format( 'ALTER TABLE %s ADD FOREIGN KEY( %I ) REFERENCES _object_reference.object', table_name, field_name ) ); +END +$body$ + , 'Add a foreign key from . to the object table.' + , 'object_reference__dependency' +); +/* + * OBJECT INFO FUNCTIONS + */ +SELECT __object_reference.create_function( + 'object_reference.object__describe' + , $args$ + object_id int +$args$ + , 'text LANGUAGE sql' + , $body$ +SELECT pg_catalog.pg_describe_object( + o.classid, + o.objid, + o.objsubid +) +FROM _object_reference._object_oid o +WHERE o.object_id = $1 +$body$ + , 'Return a human-readable description of the object, matching pg_describe_object() format.' + , 'object_reference__usage' +); + +SELECT __object_reference.create_function( + 'object_reference.object__identity' + , $args$ + object_id int + , OUT type text + , OUT schema text + , OUT name text + , OUT identity text +$args$ + , 'record LANGUAGE sql' + , $body$ +SELECT + i.type::text, + i.schema::text, + i.name::text, + i.identity::text +FROM _object_reference._object_oid o, + LATERAL pg_catalog.pg_identify_object(o.classid, o.objid, o.objsubid) i +WHERE o.object_id = $1 +$body$ + , 'Return object identification information matching pg_identify_object() format.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object__cleanup' + , $args$ + object_id int +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + DELETE FROM _object_reference.object WHERE object.object_id = object__cleanup.object_id; +EXCEPTION WHEN foreign_key_violation THEN + -- Object is still referenced elsewhere, ignore the error + NULL; +END +$body$ + , 'Attempts to delete an object from the tracking system. Silently returns if the object is still referenced by other tables.' + , 'object_reference__usage' +); +/* + * OBJECT GETSERT + */ +SELECT __object_reference.create_function( + '_object_reference._object_v__for_update' + , $args$ + object_type _object_reference.object.object_type%TYPE + , objid _object_reference._object_oid.objid%TYPE + , objsubid _object_reference._object_oid.objsubid%TYPE + , object_group_id int DEFAULT NULL + , class_id regclass DEFAULT NULL +$args$ + , '_object_reference._object_v LANGUAGE plpgsql' + , $body$ +DECLARE + c_classid CONSTANT regclass := cat_tools.object__address_classid(object_type); + + r_object_v _object_reference._object_v; + r_address record; + r_identity record; + + did_insert boolean := false; + + i smallint; + sql text; +BEGIN + ASSERT class_id IS NULL OR class_id = c_classid, format( + 'cat_tools.object__address_classid(object_type) %L <> class_id %L' + , c_classid + , class_id + ); + IF object_reference.unsupported(object_type) THEN + RAISE 'object_type % is not supported', object_type; + END IF; + + SELECT INTO r_address * FROM pg_catalog.pg_identify_object_as_address(c_classid, objid, objsubid); + + IF r_address IS NULL THEN + RAISE 'unable to find object' + USING DETAIL = format( + 'pg_identify_object_as_address(%s, %s, %s) returned NULL' + , c_classid + , objid + , objsubid + ) + ; + END IF; + + -- Refuse to track objects in temporary schemas + SELECT INTO r_identity * FROM pg_catalog.pg_identify_object(c_classid, objid, objsubid); + IF r_identity.schema IS NOT NULL AND (r_identity.schema LIKE 'pg_temp%' OR r_identity.schema LIKE 'pg_toast_temp%') THEN + RAISE 'cannot track temporary object' + USING DETAIL = format('object %s is in temporary schema %s', r_identity.identity, r_identity.schema) + , ERRCODE = 'feature_not_supported' + ; + END IF; + + -- Ensure the object record exists + SELECT INTO r_object_v + * + FROM _object_reference._object_v__for_update o + WHERE (o.object_type, o.object_names, o.object_args) = (_object_v__for_update.object_type, r_address.object_names, r_address.object_args) + ; + IF NOT FOUND THEN + FOR i IN 1..10 LOOP + did_insert := true; + INSERT INTO _object_reference.object(object_type, object_names, object_args) + VALUES(_object_v__for_update.object_type, r_address.object_names, r_address.object_args) + ON CONFLICT ON CONSTRAINT object__u_object_names__object_args DO NOTHING + ; + -- Still a small race condition here... + SELECT INTO r_object_v + * + FROM _object_reference._object_v__for_update o + WHERE (o.object_type, o.object_names, o.object_args) = (_object_v__for_update.object_type, r_address.object_names, r_address.object_args) + ; + EXIT WHEN FOUND; + END LOOP; + IF NOT FOUND THEN + RAISE 'fell out of loop!' USING HINT = 'This should never happen.'; + END IF; + END IF; + + ASSERT r_object_v.names_ok, 'names do not match (should not be possible)' ; + + IF object_group_id IS NOT NULL THEN + PERFORM object_reference.object_group__object__add(object_group_id, r_object_v.object_id); + END IF; + + -- Handle _object_oid table + CASE + WHEN r_object_v.ids_ok THEN + RETURN r_object_v; + + WHEN NOT r_object_v.ids_exist THEN + /* + * Just need to create IDs record. + */ + + /* + * This shouldn't normally happen, but could occur if a restore didn't + * finish cleanly. We know it's safe to do this because names_ok is true. + */ + IF NOT did_insert THEN + RAISE WARNING 'missing record in _object_reference._object_oid for object_id %', r_object_v.object_id + USING HINT = 'This indicates a restore did not finish cleanly.' + ; + END IF; + r_object_v := _object_reference._object_oid__add(r_object_v.object_id, object_type, c_classid, objid, objsubid); + + WHEN r_object_v.ids_exist THEN + RAISE 'ids are out of sync for object_id %', r_object_v.object_id + USING DETAIL = format( + E'_object_reference._object_v = %L,\n arguments (%L, %s, %s, %s)' + , pg_catalog.row_to_json(r_object_v, true) + , object_type + , objid + , objsubid + , object_group_id + ) + , HINT = 'this shoud not happen if event trigger "zzz_object_reference_end" is working' + ; + ELSE + RAISE 'unknown condition'; + END CASE; + + RETURN r_object_v; +END +$body$ + , 'Return details of a object record, creating a new record if one does not exist.' +); + +SELECT __object_reference.create_function( + 'object_reference.object__getsert_w_group_id' + , $args$ + object_type cat_tools.object_type + , object_name text + , secondary text DEFAULT NULL + , object_group_id int DEFAULT NULL + , loose boolean DEFAULT false +$args$ + , 'int LANGUAGE plpgsql' + , $body$ +DECLARE + c_catalog CONSTANT regclass := cat_tools.object__catalog(object_type); + c_loose CONSTANT boolean := coalesce(loose, false); + + v_objid oid; + v_subid int := 0; +BEGIN + RAISE DEBUG '% "%" (secondary %) uses catalog %', object_type, object_name, secondary, c_catalog; + + -- Some catalogs need special handling + CASE c_catalog + -- Functions + WHEN 'pg_catalog.pg_proc'::regclass THEN + /* + * Need to handle functions specially to support all the extra options they + * can have that regprocedure doesn't support. + */ + -- TODO: allow this to parse object_name directly + BEGIN + v_objid := cat_tools.regprocedure(object_name, secondary); + EXCEPTION WHEN undefined_function THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE; + END; + secondary = NULL; + + -- Columns + WHEN 'pg_catalog.pg_attribute'::regclass THEN + v_objid := object_name::regclass; + BEGIN + -- Will throw error if column isn't valid + v_subid := (cat_tools.pg_attribute__get(v_objid, secondary)).attnum; + EXCEPTION WHEN undefined_column THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE; + END; + secondary = NULL; + + -- Defaults + WHEN 'pg_catalog.pg_attrdef'::regclass THEN + BEGIN + SELECT INTO STRICT v_objid + oid + FROM pg_catalog.pg_attrdef + WHERE adrelid = object_name::regclass + -- Will throw error if column isn't valid + AND adnum = (cat_tools.pg_attribute__get(object_name::regclass, secondary)).attnum + ; + EXCEPTION WHEN no_data_found THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE 'default value for %.% does not exist', object_name::regclass, secondary + USING ERRCODE = 'undefined_object' + ; + END; + secondary = NULL; + + -- Triggers + WHEN 'pg_catalog.pg_trigger'::regclass THEN + BEGIN + SELECT INTO STRICT v_objid + oid + FROM pg_catalog.pg_trigger + WHERE tgrelid = object_name::regclass + AND tgname = secondary + ; + EXCEPTION WHEN no_data_found THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE 'trigger "%" for table "%" does not exist', secondary, object_name::regclass + USING ERRCODE = 'undefined_object' + ; + END; + secondary = NULL; + + -- Constraints + WHEN 'pg_catalog.pg_constraint'::regclass THEN + DECLARE + v_relid oid = 0; + v_typid oid = 0; + BEGIN + CASE object_type + WHEN 'table constraint'::cat_tools.object_type THEN -- conrelid + v_relid := object_name::regclass; + WHEN 'domain constraint'::cat_tools.object_type THEN -- contypid + v_typid := object_name::regtype; + ELSE + RAISE 'unexpected object type % for a constraint', object_type; + END CASE; + + BEGIN + SELECT INTO STRICT v_objid + oid + FROM pg_catalog.pg_constraint + WHERE conname = secondary + AND conrelid = v_relid + AND contypid = v_typid + ; + EXCEPTION WHEN no_data_found THEN + -- At this point regclass or regtype should have thrown an error if the parent object doesn't exist + IF c_loose THEN + RETURN NULL; + END IF; + RAISE 'constraint "%" does not exist', secondary + USING ERRCODE = 'undefined_object' + ; + END; + END; + secondary = NULL; + + -- Casts + WHEN 'pg_catalog.pg_cast'::regclass THEN + BEGIN + SELECT INTO STRICT v_objid + oid + FROM pg_catalog.pg_cast + WHERE castsource = object_name::regtype + AND casttarget = secondary::regtype + ; + EXCEPTION WHEN no_data_found THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE 'cast from "%" to "%" does not exist', object_name, secondary + USING ERRCODE = 'undefined_object' + ; + IF c_loose THEN + RETURN NULL; + END IF; + END; + secondary = NULL; + + ELSE + DECLARE + c_reg_type name := cat_tools.object__reg_type(c_catalog); + + v_name_field text; + sql text; + BEGIN + IF c_reg_type IS NULL THEN + /* + * Need to do a manual lookup of the OID based on what catalog it is + * + * Get first 3 letters of catalog name after the 'pg_', since that's + * usually the field name. We also need to handle the possibility of + * 'pg_catalog.' being part of c_catalog. + */ + v_name_field := substring(regexp_replace(c_catalog::text, '(pg_catalog\.)?pg_', ''), 1, 3); + + sql := format( + 'SELECT oid FROM %s WHERE %I = %L' + , c_catalog -- No need to quote + , v_name_field + , object_name + ); + ELSE + sql := format( + 'SELECT %L::%s' + , object_name + , c_reg_type -- No need to quote + ); + END IF; + RAISE DEBUG 'looking up % % via %', object_type, object_name, sql; + BEGIN + EXECUTE sql INTO STRICT v_objid; + EXCEPTION WHEN no_data_found THEN + IF c_loose THEN + RETURN NULL; + END IF; + RAISE '% "%" does not exist', object_type, object_name + USING ERRCODE = 'undefined_object' + ; + END; + END; + END CASE; + + IF secondary IS NOT NULL THEN + RAISE 'secondary may not be specified for % objects', object_type; + END IF; + + RETURN (_object_reference._object_v__for_update( object_type, v_objid, v_subid, object_group_id )).object_id; +END +$body$ + , 'Return a object_id for an object. Allows specifying a object group ID to add the object to. See also object__getsert().' + , 'object_reference__usage' +); + +SELECT __object_reference.create_function( + 'object_reference.object__getsert' + , $args$ + object_type cat_tools.object_type + , object_name text + , secondary text DEFAULT NULL + , object_group_name _object_reference.object_group.object_group_name%TYPE DEFAULT NULL + , loose boolean DEFAULT false +$args$ + , 'int LANGUAGE sql' + , $body$ +SELECT object_reference.object__getsert_w_group_id( + $1, $2, $3 + , CASE WHEN object_group_name IS NOT NULL THEN + (object_reference.object_group__get($4)).object_group_id + END + , $5 +) +$body$ + , 'Return a object_id for an object. Allows specifying a object group name to add the object to. See also object__getsert_w_group_id().' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object__getsert' + , $args$ + object_type text + , object_name text + , secondary text DEFAULT NULL + , object_group_name _object_reference.object_group.object_group_name%TYPE DEFAULT NULL + , loose boolean DEFAULT false +$args$ + , 'int LANGUAGE sql' + , $$SELECT object_reference.object__getsert( lower($1)::cat_tools.object_type, $2, $3, $4, $5 )$$ + , 'Return a object_id for an object. Allows specifying a object group name to add the object to. See also object__getsert_w_group_id().' + , 'object_reference__usage' +); + +/* + * ddl_capture + */ +SELECT __object_reference.create_function( + 'object_reference.capture__get_all' + , $args$ + OUT capture_level int + , OUT object_group_id _object_reference.object_group.object_group_id%TYPE + , OUT object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , 'SETOF RECORD LANGUAGE plpgsql' + , $body$ +DECLARE +BEGIN + RETURN QUERY SELECT c.capture_level, c.object_group_id, og.object_group_name + FROM pg_temp.__object_reference__ddl_capture c + JOIN _object_reference.object_group og USING(object_group_id) + ORDER BY capture_level DESC + ; +EXCEPTION WHEN undefined_table THEN + RETURN; +END +$body$ + , 'Return stack of object groups that are being captured to.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.capture__get_current' + , $args$ + OUT capture_level int + , OUT object_group_id _object_reference.object_group.object_group_id%TYPE + , OUT object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , 'RECORD LANGUAGE sql' + , $body$ +SELECT * FROM object_reference.capture__get_all() LIMIT 1 +$body$ + , 'Return object group that object creation currently is being captured to.' + , 'object_reference__usage' +); + +SELECT __object_reference.create_function( + 'object_reference.capture__start' + , $args$ + object_group_id _object_reference.object_group.object_group_id%TYPE +$args$ + , 'int SECURITY DEFINER LANGUAGE plpgsql' + , $body$ +DECLARE + c_next_level int := coalesce(capture_level, 0) + 1 FROM object_reference.capture__get_current(); +BEGIN + -- Ensure object group exists + PERFORM object_reference.object_group__get(object_group_id); + + INSERT INTO pg_temp.__object_reference__ddl_capture + SELECT c_next_level, capture__start.object_group_id + ; + RETURN c_next_level; + +EXCEPTION WHEN undefined_table THEN + /* + CREATE TEMP TABLE __object_reference__ddl_capture AS + SELECT c_next_level, capture__start.object_group_id + ; + */ + CREATE TEMP TABLE __object_reference__ddl_capture( + capture_level int PRIMARY KEY + , object_group_id INT NOT NULL -- temp tables can't reference permanent ones + ); + -- This breaks if run directly under plpgsql + EXECUTE $code$ + CREATE CONSTRAINT TRIGGER verify_capture_stop AFTER INSERT + ON pg_temp.__object_reference__ddl_capture + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW -- CONSTRAINT triggers must be per-ROW + EXECUTE PROCEDURE _object_reference._tg_capture_safety() + $code$; + + INSERT INTO pg_temp.__object_reference__ddl_capture + SELECT c_next_level, capture__start.object_group_id + ; + RETURN c_next_level; +END +$body$ + , 'Begin capturing newly created objects to . Returns current capture level.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.capture__start' + , $args$ + object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , 'int LANGUAGE sql' + , $body$ +SELECT object_reference.capture__start( + (object_reference.object_group__get(object_group_name)).object_group_id +); +$body$ + , 'Begin capturing newly created objects to . Returns current capture level.' + , 'object_reference__usage' +); + +SELECT __object_reference.create_function( + 'object_reference.capture__stop' + , $args$ + object_group_id _object_reference.object_group.object_group_id%TYPE +$args$ + , 'void SECURITY DEFINER LANGUAGE plpgsql' + , $body$ +DECLARE + r record; +BEGIN + SELECT INTO STRICT r + * + FROM object_reference.capture__get_current() + ; + IF r.capture_level IS NULL THEN + RAISE 'not capturing DDL' + USING HINT = 'Did you forget to call object_referenc.capture__start()?' + -- TODO: use better status code + ; + END IF; + + IF r.object_group_id <> coalesce(object_group_id) THEN + RAISE 'object_group mismatch' + USING DETAIL = format( + 'currently capturing for group %L (id %s), expecting group %L (id %s)' + , r.object_group_name + , r.object_group_id + -- This will error if the group doesn't exist + , (object_reference.object_group__get(object_group_id)).object_group_name + , capture__stop.object_group_id + ) + ; + END IF; + + DELETE FROM pg_temp.__object_reference__ddl_capture WHERE capture_level = r.capture_level; +EXCEPTION WHEN undefined_table THEN + RAISE 'not capturing DDL' + USING HINT = 'Did you forget to call object_referenc.capture__start()?' + ; +END +$body$ + , 'Begin capturing newly created objects to . Returns current capture level.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.capture__stop' + , $args$ + object_group_name _object_reference.object_group.object_group_name%TYPE +$args$ + , 'void LANGUAGE sql' + , $body$ +SELECT object_reference.capture__stop( + (object_reference.object_group__get(object_group_name)).object_group_id +) +$body$ + , 'Begin capturing newly created objects to . Returns current capture level.' + , 'object_reference__usage' +); + +SELECT __object_reference.create_function( + '_object_reference._tg_capture_safety' + , '' + , 'trigger LANGUAGE plpgsql' + , $body$ +BEGIN + IF EXISTS(SELECT 1 FROM pg_temp.__object_reference__ddl_capture) THEN + RAISE 'attempted commit while still capturing DDL' + USING HINT = 'Did you not start a transaction? Did you forget to call object_reference.capture__stop()?' + ; + END IF; + + RETURN NULL; +END +$body$ + , 'Trigger function to ensure capture__stop() is called an appropriate number of times.' +); +SELECT __object_reference.create_function( + '_object_reference._etg_capture' + , '' + , 'event_trigger SECURITY DEFINER LANGUAGE plpgsql' + , $body$ +DECLARE + c_group_id CONSTANT int := object_group_id FROM object_reference.capture__get_current(); + r record; +BEGIN + + IF c_group_id IS NOT NULL THEN -- Would be NULL if table is empty + RAISE DEBUG E'\n\n*** START ***'; + BEGIN + FOR r IN + SELECT classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension + -- Have to manually exclude command field :/ + FROM pg_catalog.pg_event_trigger_ddl_commands() + LOOP + RAISE DEBUG 'ddl: %', row_to_json(r); + END LOOP; + END; + + FOR r IN SELECT + _object_reference._object_v__for_update( + object_type::cat_tools.object_type + , objid, objsubid + , c_group_id + , classid + ) + , classid, objid, objsubid, command_tag, object_type, schema_name, object_identity, in_extension + FROM pg_catalog.pg_event_trigger_ddl_commands() + WHERE command_tag ~ '^CREATE' --'^(ALTER|CREATE)' + AND NOT object_reference.unsupported(object_type::cat_tools.object_type) + AND (schema_name IS NULL + OR schema_name NOT LIKE 'pg_temp%' -- pg_my_temp_schema() doesn't seem worth it... + ) + LOOP + RAISE DEBUG 'registered %', row_to_json(r); + END LOOP; + RAISE DEBUG E'*** END ***\n\n'; + END IF; +END +$body$ + , 'Event trigger function to capture newly created objects in an object group.' +); + + +SELECT __object_reference.create_function( + '_object_reference._etg_fix_identity' + , '' + , 'event_trigger SECURITY DEFINER LANGUAGE plpgsql' + , $body$ +DECLARE + r_ddl record; + r record; +BEGIN + /* + * It's tempting to use pg_event_trigger_ddl_commands() to find exactly what + * items have changed and worry about only those. That won't work because an + * object_names array can depend on multiple names (ie: a column depends on + * the name of it's table, as well as the name of the schema the table is in. + * You might think we could simply recurse through pg_depend to handle this, + * but not every name dependency gets enumerated that way. For example, + * columns are not marked as dependent on their table. + * + * Rather than trying to be cute about this, we just do a brute-force check + * for any names that have changed. + */ + + /* + * Presumably there's no way for an objects type/classid to change, but be + * safe and attempt the update to object_type. If it actually does change the + * constraint on the table should catch it. + */ + FOR r IN + UPDATE _object_reference.object + SET object_type = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).type::cat_tools.object_type + , object_names = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_names + , object_args = (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)).object_args + FROM _object_reference._object_oid oo + WHERE + oo.object_id = object.object_id + AND (object_type::text, object_names, object_args) IS DISTINCT FROM + (pg_catalog.pg_identify_object_as_address(classid, objid, objsubid)) + RETURNING * + LOOP + RAISE DEBUG 'modified_objects(): %', r; + END LOOP; +END +$body$ + , 'Event trigger function to update any records with object names or args that have changed.' +); +SELECT __object_reference.create_function( + '_object_reference._etg_drop' + , '' + , 'event_trigger LANGUAGE plpgsql' + , $body$ +DECLARE + r_object_v _object_reference._object_v; + r record; +BEGIN + FOR r IN SELECT classid, objid, objsubid, object_type, schema_name, object_identity FROM pg_catalog.pg_event_trigger_dropped_objects() LOOP + RAISE DEBUG 'dropped_objects(): %', r; + END LOOP; + + -- Multiple objects might have been affected + -- Could potentially be done with a writable CTE + FOR r_object_v IN + SELECT _object_v.* + FROM pg_catalog.pg_event_trigger_dropped_objects() d + JOIN _object_reference._object_v USING( classid, objid ) -- Intentionally ignore objsubid + WHERE + /* + * If an object that contains subobjects is being removed, we need to + * also remove all subobjects. In this case, we know d.objsubid = 0 + */ + d.objsubid = 0 + + /* + * Otherwise, only remove the appropriate suboject. + */ + OR d.objsubid = _object_v.objsubid + LOOP + RAISE DEBUG 'deleting object %', r_object_v; + -- TODO: trap FK violation error on groups and output something better + DELETE FROM _object_reference.object WHERE object_id = r_object_v.object_id; + END LOOP; + + /* + * We know that a restore will never drop objects, so force _object_v to be + * correct at this point. We can't do this before we delete based on the drop + * though. + */ + PERFORM object_reference.post_restore(); +END +$body$ + , 'Event trigger function to drop object records when objects are removed.' +); + +SELECT __object_reference.create_function( + '_object_reference.etg_raise__start' + , '' + , 'event_trigger LANGUAGE plpgsql' + , $body$ +BEGIN + RAISE WARNING 'etg_raise__start: % %', tg_event, tg_tag; +END; +$body$ + , $$Event trigger function to report on DDL activity. Example trigger: +CREATE EVENT TRIGGER start + ON ddl_command_start + --WHEN tag IN ( 'ALTER TABLE', 'DROP TABLE' ) + EXECUTE PROCEDURE _object_reference.etg_raise__start() +; +$$); +SELECT __object_reference.create_function( + '_object_reference.etg_raise__drop' + , '' + , 'event_trigger LANGUAGE plpgsql' + , $body$ +DECLARE + r record; +BEGIN + FOR r IN SELECT classid, objid, objsubid, object_type, schema_name, object_name, object_identity FROM pg_catalog.pg_event_trigger_dropped_objects() LOOP + RAISE WARNING 'dropped_objects: + classid: % + objid: % + objsubid: % + object_type: % + schema_name: % + object_name: % + object_identity: % + ' + -- :^r" s/\([^ ]\+\):.*/, r.\1/ + , r.classid + , r.objid + , r.objsubid + , r.object_type + , r.schema_name + , r.object_name + , r.object_identity + ; + END LOOP; +END; +$body$ + , $$Event trigger function to report on DDL activity. Example trigger: +CREATE EVENT TRIGGER drop + ON sql_drop + --WHEN tag IN ( 'ALTER TABLE', 'DROP TABLE' ) + EXECUTE PROCEDURE _object_reference.etg_raise__drop() +; +$$); + +CREATE EVENT TRIGGER zzz__object_reference_drop + ON sql_drop + -- For debugging + --WHEN tag IN ( 'ALTER TABLE', 'DROP TABLE' ) + EXECUTE PROCEDURE _object_reference._etg_drop() +; +CREATE EVENT TRIGGER zzz_object_reference__fix_identity + ON ddl_command_end + -- For debugging + --WHEN tag IN ( 'ALTER TABLE', 'DROP TABLE' ) + EXECUTE PROCEDURE _object_reference._etg_fix_identity() +; +CREATE EVENT TRIGGER zzz_object_reference_capture + ON ddl_command_end + -- For debugging + --WHEN tag IN ( 'ALTER TABLE', 'DROP TABLE' ) + EXECUTE PROCEDURE _object_reference._etg_capture() +; + +/* + * Drop "temporary" objects + */ +DROP FUNCTION __object_reference.create_function( + function_name text + , args text + , options text + , body text + , comment text + , grants text +); +DROP FUNCTION __object_reference.safe_dump( + relation regclass + , text +); +DROP FUNCTION __object_reference.exec( + sql text +); +DROP SCHEMA __object_reference; + +-- vi: expandtab sw=2 ts=2 diff --git a/sql/object_reference.sql b/sql/object_reference.sql index e83b461..5883805 100644 --- a/sql/object_reference.sql +++ b/sql/object_reference.sql @@ -2,35 +2,6 @@ \echo You really, REALLY do NOT want to try and load this via psql!!! \echo It will FAIL during pg_dump! \quit --- This BS is because count_nulls is relocatable, so could be in any schema -DO $$ -BEGIN - RAISE DEBUG 'initial search_path = %', current_setting('search_path'); - PERFORM set_config('search_path', current_setting('search_path') || ', ' || extnamespace::regnamespace::text, true) -- true = local only - FROM pg_extension - WHERE extname = 'count_nulls' - ; - RAISE DEBUG 'search_path changed to %', current_setting('search_path'); -END -$$; -/* -DO $$ -DECLARE - c_schema CONSTANT name := (SELECT extnamespace::regnamespace::text FROM pg_extension WHERE extname = 'cat_tools'); -BEGIN - IF c_schema IS NULL THEN - RAISE 'extension cat_tools is not installed'; - END IF; - - IF c_schema <> 'cat_tools' THEN - RAISE 'having the cat_tools extension installed anywhere but the "cat_tools" schema is not currently supported' - USING DETAIL = format('current schema for cat_tools is %s', c_schema) - ; - END IF; -END -$$; -*/ - DO $$ BEGIN CREATE ROLE object_reference__usage NOLOGIN; @@ -84,7 +55,7 @@ CREATE FUNCTION __object_reference.create_function( , grants text DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $body$ DECLARE - c_clean_args text := cat_tools.function__arg_types_text(args); + c_clean_args text := cat_tools.routine__parse_arg_types_text(args); create_template CONSTANT text := $template$ CREATE OR REPLACE FUNCTION %s( @@ -191,7 +162,7 @@ GRANT REFERENCES ON _object_reference.object TO object_reference__dependency; CREATE TABLE _object_reference._object_oid( object_id int PRIMARY KEY REFERENCES _object_reference.object ON DELETE CASCADE ON UPDATE CASCADE - , classid regclass NOT NULL + , classid oid NOT NULL /* TODO: needs to be a trigger CONSTRAINT classid_must_match__object__address_classid CHECK( classid IS NOT DISTINCT FROM cat_tools.object__address_classid(object_type) ) @@ -199,50 +170,11 @@ CREATE TABLE _object_reference._object_oid( , objid oid NOT NULL , objsubid int NOT NULL CONSTRAINT objid_must_match CHECK( -- _object_reference._sanity() depends on this! - objid IS NOT DISTINCT FROM coalesce( - regclass::oid -- Need to cast first item to generic OID - , regconfig - , regdictionary - , regnamespace -- SED: REQUIRES 9.5! - , regoperator - , regprocedure - , regtype - , object_oid - ) + objid IS NOT DISTINCT FROM object_oid ) , CONSTRAINT object__u_classid__objid__objsubid UNIQUE( classid, objid, objsubid ) - , regclass regclass - CONSTRAINT regclass_classid CHECK( regclass IS NULL OR classid = cat_tools.object__reg_type_catalog('regclass') ) - , regconfig regconfig - CONSTRAINT regconfig_classid CHECK( regconfig IS NULL OR classid = cat_tools.object__reg_type_catalog('regconfig') ) - , regdictionary regdictionary - CONSTRAINT regdictionary_classid CHECK( regdictionary IS NULL OR classid = cat_tools.object__reg_type_catalog('regdictionary') ) - , regnamespace regnamespace -- SED: REQUIRES 9.5! - CONSTRAINT regnamespace_classid CHECK( regnamespace IS NULL OR classid = cat_tools.object__reg_type_catalog('regnamespace') ) -- SED: REQUIRES 9.5! - , regoperator regoperator - CONSTRAINT regoperator_classid CHECK( regoperator IS NULL OR classid = cat_tools.object__reg_type_catalog('regoperator') ) - , regprocedure regprocedure - CONSTRAINT regprocedure_classid CHECK( regprocedure IS NULL OR classid = cat_tools.object__reg_type_catalog('regprocedure') ) - -- I don't think we should ever have regrole since we can't create event triggers on it --- , regrole regrole - , regtype regtype - CONSTRAINT regtype_classid CHECK( regtype IS NULL OR classid = cat_tools.object__reg_type_catalog('regtype') ) - , object_oid oid + , object_oid oid NOT NULL ); -CREATE TRIGGER null_count - AFTER INSERT OR UPDATE - ON _object_reference._object_oid - FOR EACH ROW EXECUTE PROCEDURE not_null_count_trigger( - 5 -- First 4 fields, + 1 - , 'only one object reference field may be set' - ) -; -CREATE UNIQUE INDEX _object_oid__u_regclass ON _object_reference._object_oid(regclass) WHERE regclass IS NOT NULL; -CREATE UNIQUE INDEX _object_oid__u_regconfig ON _object_reference._object_oid(regconfig) WHERE regconfig IS NOT NULL; -CREATE UNIQUE INDEX _object_oid__u_regdictionary ON _object_reference._object_oid(regdictionary) WHERE regdictionary IS NOT NULL; -CREATE UNIQUE INDEX _object_oid__u_regoperator ON _object_reference._object_oid(regoperator) WHERE regoperator IS NOT NULL; -CREATE UNIQUE INDEX _object_oid__u_regprocedure ON _object_reference._object_oid(regprocedure) WHERE regprocedure IS NOT NULL; -CREATE UNIQUE INDEX _object_oid__u_regtype ON _object_reference._object_oid(regtype) WHERE regtype IS NOT NULL; SELECT __object_reference.create_function( '_object_reference._sanity' @@ -302,13 +234,6 @@ CREATE VIEW _object_reference._object_v AS , i.classid , i.objid , i.objsubid - , i.regclass - , i.regconfig - , i.regdictionary - , i.regnamespace - , i.regoperator - , i.regprocedure - , i.regtype , i.object_oid , s.* FROM _object_reference.object o @@ -324,13 +249,6 @@ CREATE VIEW _object_reference._object_v__for_update AS , i.classid , i.objid , i.objsubid - , i.regclass - , i.regconfig - , i.regdictionary - , i.regnamespace - , i.regoperator - , i.regprocedure - , i.regtype , i.object_oid , s.* FROM _object_reference.object o @@ -362,26 +280,9 @@ BEGIN WHERE o.object_id = _object_oid__add.object_id ; END IF; - DECLARE - c_reg_type name := cat_tools.object__reg_type(object_type); -- Verifies regtype is supported, if there is one - c_oid_field CONSTANT name := coalesce(c_reg_type, 'object_oid'); - - c_oid_insert CONSTANT text := format( - --USING object_id, classid, objid, objsubid - $$INSERT INTO _object_reference._object_oid(object_id, classid, objid, objsubid, %I) - SELECT $1, $2, $3, $4, $3::%I$$ - , c_oid_field - , coalesce(c_reg_type, 'oid') - ) - ; BEGIN - RAISE DEBUG E'%\n USING %, %, %, %' - , c_oid_insert - , object_id, classid, objid, objsubid - ; - EXECUTE c_oid_insert - USING object_id, classid, objid, objsubid - ; + INSERT INTO _object_reference._object_oid(object_id, classid, objid, objsubid, object_oid) + VALUES (object_id, classid, objid, objsubid, objid); SELECT INTO STRICT r_object_v -- Record better exist! * @@ -522,7 +423,7 @@ SELECT __object_reference.create_function( , $body$ SELECT cat_tools.objects__shared() || cat_tools.objects__address_unsupported() - || '{event trigger}' + || '{event trigger, partitioned table, partitioned index}' $body$ , 'Returns array of object types that are not supported.' , 'object_reference__usage' @@ -626,6 +527,23 @@ CREATE TABLE _object_reference.object_group__object( ); SELECT __object_reference.safe_dump('_object_reference.object_group__object'); +-- Trigger function for automatic object cleanup +SELECT __object_reference.create_function( + '_object_reference._object_group__object__cleanup_trigger' + , '' + , 'trigger LANGUAGE plpgsql' + , $body$ +BEGIN + PERFORM object_reference.object__cleanup(OLD.object_id); + RETURN OLD; +END +$body$ + , 'Trigger function to automatically attempt cleanup of objects when removed from groups.' +); +CREATE TRIGGER object_group__object__cleanup + AFTER DELETE ON _object_reference.object_group__object + FOR EACH ROW + EXECUTE FUNCTION _object_reference._object_group__object__cleanup_trigger(); -- __get SELECT __object_reference.create_function( 'object_reference.object_group__get' @@ -829,7 +747,68 @@ $body$ , 'Add a foreign key from . to the object table.' , 'object_reference__dependency' ); +/* + * OBJECT INFO FUNCTIONS + */ +SELECT __object_reference.create_function( + 'object_reference.object__describe' + , $args$ + object_id int +$args$ + , 'text LANGUAGE sql' + , $body$ +SELECT pg_catalog.pg_describe_object( + o.classid, + o.objid, + o.objsubid +) +FROM _object_reference._object_oid o +WHERE o.object_id = $1 +$body$ + , 'Return a human-readable description of the object, matching pg_describe_object() format.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object__identity' + , $args$ + object_id int + , OUT type text + , OUT schema text + , OUT name text + , OUT identity text +$args$ + , 'record LANGUAGE sql' + , $body$ +SELECT + i.type::text, + i.schema::text, + i.name::text, + i.identity::text +FROM _object_reference._object_oid o, + LATERAL pg_catalog.pg_identify_object(o.classid, o.objid, o.objsubid) i +WHERE o.object_id = $1 +$body$ + , 'Return object identification information matching pg_identify_object() format.' + , 'object_reference__usage' +); +SELECT __object_reference.create_function( + 'object_reference.object__cleanup' + , $args$ + object_id int +$args$ + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + DELETE FROM _object_reference.object WHERE object.object_id = object__cleanup.object_id; +EXCEPTION WHEN foreign_key_violation THEN + -- Object is still referenced elsewhere, ignore the error + NULL; +END +$body$ + , 'Attempts to delete an object from the tracking system. Silently returns if the object is still referenced by other tables.' + , 'object_reference__usage' +); /* * OBJECT GETSERT */ @@ -849,6 +828,7 @@ DECLARE r_object_v _object_reference._object_v; r_address record; + r_identity record; did_insert boolean := false; @@ -877,6 +857,15 @@ BEGIN ; END IF; + -- Refuse to track objects in temporary schemas + SELECT INTO r_identity * FROM pg_catalog.pg_identify_object(c_classid, objid, objsubid); + IF r_identity.schema IS NOT NULL AND (r_identity.schema LIKE 'pg_temp%' OR r_identity.schema LIKE 'pg_toast_temp%') THEN + RAISE 'cannot track temporary object' + USING DETAIL = format('object %s is in temporary schema %s', r_identity.identity, r_identity.schema) + , ERRCODE = 'feature_not_supported' + ; + END IF; + -- Ensure the object record exists SELECT INTO r_object_v * diff --git a/test/deps.sql b/test/deps.sql index e1a53c8..d13a016 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -4,6 +4,5 @@ /* * Normally these should be loaded by the cascade! -CREATE EXTENSION IF NOT EXISTS count_nulls; CREATE EXTENSION IF NOT EXISTS cat_tools; */ diff --git a/test/dump/run.sh b/test/dump/run.sh index 4d0c7a3..6896f68 100755 --- a/test/dump/run.sh +++ b/test/dump/run.sh @@ -26,7 +26,7 @@ if [ "$1" == "-f" ]; then fi echo Creating dump database -createdb test_dump && psql -f test/dump/load_all.sql test_dump > $create_log || die 3 "Unable to create dump database" +createdb test_dump && psql -Xf test/dump/load_all.sql test_dump > $create_log || die 3 "Unable to create dump database" # Ensure no errors in log check_log() { @@ -45,8 +45,8 @@ check_log $create_log creation echo Running dump and restore # No real need to cat the log on failure here; psql will generate an error and even if not verify will almost certainly catch it -createdb test_load && PAGER='' psql -c '\df pg_get_object_address' test_load || die 5 'crap' -(echo 'BEGIN;' && pg_dump test_dump && echo 'COMMIT;') | psql -q -v VERBOSITY=verbose -v ON_ERROR_STOP=true test_load > $restore_log +createdb test_load && PAGER='' psql -Xc '\df pg_get_object_address' test_load || die 5 'crap' +(echo 'BEGIN;' && pg_dump test_dump && echo 'COMMIT;') | psql -q -X -v VERBOSITY=verbose -v ON_ERROR_STOP=true test_load > $restore_log rc=$? if [ $rc -ne 0 ]; then cat $restore_log @@ -54,7 +54,7 @@ if [ $rc -ne 0 ]; then fi echo Verifying restore -psql -f test/dump/verify.sql test_load > $verify_log || die 5 "Test failed" +psql -Xf test/dump/verify.sql test_load > $verify_log || die 5 "Test failed" check_log $create_log verify diff --git a/test/expected/base.out b/test/expected/base.out index 5e39f56..5357054 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -1,12 +1,13 @@ \set ECHO none -1..9 +1..10 ok 1 - Role object_reference__dependency should be granted USAGE on schema _object_reference ok 2 - Role object_reference__dependency should be granted REFERENCES on table _object_reference.object ok 3 - CREATE TEMP TABLE test_object AS SELECT object_reference.object__getsert('table', 'test_table') AS object_id; -ok 4 - Verify regclass field is correct -ok 5 - Existing object works, provides correct ID -ok 6 - secondary may not be specified for table objects -ok 7 - Verify count_nulls extension can not be relocated -ok 8 - Still works after moving the count_nulls extension -ok 9 - CREATE EXTENSION test_factory +ok 4 - Verify object_oid field is correct +ok 5 - object__describe returns same result as pg_describe_object +ok 6 - object__identity returns same result as pg_identify_object +ok 7 - Existing object works, provides correct ID +ok 8 - secondary may not be specified for table objects +ok 9 - temp objects are rejected +ok 10 - CREATE EXTENSION test_factory # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/object_group.out b/test/expected/object_group.out index d286833..e670106 100644 --- a/test/expected/object_group.out +++ b/test/expected/object_group.out @@ -1,5 +1,5 @@ \set ECHO none -1..24 +1..29 ok 1 - Register test table 1 ok 2 - object_group__create(...) for group name that is too long throws error ok 3 - object_group__create('object reference test group') @@ -24,6 +24,9 @@ ok 21 - object_group__object__add(...)for missing group throws error ok 22 - Removing group with items in it fails ok 23 - __object__remove() for col1 works ok 24 - __object__remove() for test_table_2 works -ok 25 - Removing empty group works -# Looks like you planned 24 tests but ran 25 +ok 25 - Add test table back to group for cleanup test +ok 26 - Object exists before cleanup test +ok 27 - Remove from group triggers automatic cleanup attempt +ok 28 - Object was automatically cleaned up after group removal +ok 29 - Removing empty group works # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/zzz_build.out b/test/expected/zzz_build.out index fe92660..0996ed0 100644 --- a/test/expected/zzz_build.out +++ b/test/expected/zzz_build.out @@ -2,16 +2,26 @@ This extension must be loaded via CREATE EXTENSION object_reference; You really, REALLY do NOT want to try and load this via psql!!! -psql:test/temp_load.not_sql:188: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:158: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! -psql:test/temp_load.not_sql:189: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:159: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.classid%TYPE converted to oid +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.classid%TYPE converted to oid +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:305: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer -psql:test/temp_load.not_sql:513: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:413: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! @@ -21,28 +31,80 @@ psql:test/temp_load.not_sql:513: WARNING: I promise you will be sorry if you tr -psql:test/temp_load.not_sql:620: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:520: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! -psql:test/temp_load.not_sql:627: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:527: WARNING: I promise you will be sorry if you try to use this as anything other than an extension! +psql:test/temp_load.not_sql:571: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:571: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:596: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:596: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:611: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:611: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:638: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:638: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:654: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:654: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:671: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:671: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:671: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:671: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:705: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:705: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:705: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:705: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:941: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:1168: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1168: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1182: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1182: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1209: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1223: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying + +psql:test/temp_load.not_sql:1270: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1270: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer + +psql:test/temp_load.not_sql:1284: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1284: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying + +psql:test/temp_load.not_sql:1329: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1329: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer + +psql:test/temp_load.not_sql:1343: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1343: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying diff --git a/test/load.sql b/test/load.sql index 0f1c6be..f1b267f 100644 --- a/test/load.sql +++ b/test/load.sql @@ -1,6 +1,5 @@ \i test/pgxntool/setup.sql --- Need to add count_nulls back into the path SET search_path = tap, public; -- Don't use IF NOT EXISTS here; we want to ensure we always have the latest code diff --git a/test/sql/all.sql b/test/sql/all.sql index e00f04b..8e96fe2 100644 --- a/test/sql/all.sql +++ b/test/sql/all.sql @@ -53,6 +53,13 @@ SELECT bag_eq( UNION -- Intentionally not UNION ALL; we want to know if object_reference.unsupported has dupes SELECT * FROM cat_tools.objects__address_unsupported_srf() UNION SELECT 'event trigger' + /* + * pg_identify_object_as_address() returns these as plain "table"/"index", + * and pg_get_object_address() doesn't recognize "partitioned table" or + * "partitioned index" at all, so the round-trip is broken. + */ + UNION SELECT 'partitioned table' + UNION SELECT 'partitioned index' $$ , 'Verify object_reference.unsupported()' ); diff --git a/test/sql/base.sql b/test/sql/base.sql index 202ae5f..5fa9918 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -8,8 +8,8 @@ SELECT plan( 0 +1 -- schema +3 -- initial - +2 -- errors - +2 -- move + +2 -- new functions + +3 -- errors (includes temp object test) +1 -- create extensions ); @@ -32,9 +32,23 @@ SELECT lives_ok( , $$CREATE TEMP TABLE test_object AS SELECT object_reference.object__getsert('table', 'test_table') AS object_id;$$ ); SELECT is( - (SELECT regclass FROM _object_reference._object_v WHERE object_id = (SELECT object_id FROM test_object)) - , 'test_table'::regclass - , 'Verify regclass field is correct' + (SELECT object_oid FROM _object_reference._object_v WHERE object_id = (SELECT object_id FROM test_object)) + , 'test_table'::regclass::oid + , 'Verify object_oid field is correct' +); + +-- Test object__describe function +SELECT is( + object_reference.object__describe((SELECT object_id FROM test_object)) + , pg_catalog.pg_describe_object('pg_class'::regclass, 'test_table'::regclass, 0) + , 'object__describe returns same result as pg_describe_object' +); + +-- Test object__identity function +SELECT results_eq( + $$SELECT * FROM object_reference.object__identity((SELECT object_id FROM test_object))$$ + , $$SELECT type, schema, name, identity FROM pg_catalog.pg_identify_object('pg_class'::regclass, 'test_table'::regclass, 0)$$ + , 'object__identity returns same result as pg_identify_object' ); SELECT is( object_reference.object__getsert('table', 'test_table') @@ -50,24 +64,13 @@ SELECT throws_ok( , 'secondary may not be specified for table objects' ); -/* - * I'm not sure if our extension would continue working if count_nulls was - * relocated. Currently a moot point since relocation isn't supported, but I'd - * already coded the second test so might as well leave it here in case it - * changes in the future. - */ -\set null_schema test_relocate_count_nulls -CREATE SCHEMA :null_schema; +-- Test temp object rejection +CREATE TEMP TABLE temp_test_table(); SELECT throws_ok( - $$ALTER EXTENSION count_nulls SET SCHEMA $$ || :'null_schema' - , '0A000' - , NULL - , 'Verify count_nulls extension can not be relocated' -); -SELECT is( - object_reference.object__getsert('table', 'test_table') - , (SELECT object_id FROM test_object) - , 'Still works after moving the count_nulls extension' + $$SELECT object_reference.object__getsert('table', 'temp_test_table')$$ + , '0A000' -- feature_not_supported + , 'cannot track temporary object' + , 'temp objects are rejected' ); -- Create extensions diff --git a/test/sql/object_group.sql b/test/sql/object_group.sql index 6304892..bef342a 100644 --- a/test/sql/object_group.sql +++ b/test/sql/object_group.sql @@ -2,8 +2,8 @@ \i test/load.sql -CREATE TEMP TABLE test_table_1(col1 int, col2 int); -CREATE TEMP TABLE test_table_2(col1 int, col2 int); +CREATE TABLE object_group_test_table_1(col1 int, col2 int); +CREATE TABLE object_group_test_table_2(col1 int, col2 int); CREATE FUNCTION pg_temp.bogus_group( command_template text @@ -40,10 +40,12 @@ SELECT plan( +4 -- __object__remove +4 + 2 -- __remove + +4 -- cleanup tests + +1 -- final group removal (there was always an extra test) ); SELECT lives_ok( - $$CREATE TEMP TABLE test_table_1_id AS SELECT * FROM object_reference.object__getsert('table', 'test_table_1')$$ + $$CREATE TEMP TABLE test_table_1_id AS SELECT * FROM object_reference.object__getsert('table', 'object_group_test_table_1')$$ , 'Register test table 1' ); @@ -102,37 +104,37 @@ SELECT lives_ok( -- object__getsert SELECT throws_ok( -- Can't use helper here - $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert('table column', 'test_table_1', 'col1', 'absurd group name used only for testing purposes ktxbye')$$ + $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert('table column', 'object_group_test_table_1', 'col1', 'absurd group name used only for testing purposes ktxbye')$$ , 'P0002' , 'object group "absurd group name used only for testing purposes ktxbye" does not exist' , 'object__getsert with bogus group name' ); /* TODO SELECT throws_ok( -- Can't use helper here - $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert_w_group_id('table column', 'test_table_1', 'col1', -1)$$ + $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert_w_group_id('table column', 'object_group_test_table_1', 'col1', -1)$$ , '' , '' , 'object__getsert with bogus group id' ); */ SELECT lives_ok( - $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert('table column', 'test_table_1', 'col1', 'object reference test group')$$ + $$CREATE TEMP TABLE col1_id AS SELECT * FROM object_reference.object__getsert('table column', 'object_group_test_table_1', 'col1', 'object reference test group')$$ , 'Register test column' ); SELECT lives_ok( - $$CREATE TEMP TABLE test_table_2_id AS SELECT * FROM object_reference.object__getsert('table', 'test_table_2', object_group_name := 'object reference test group')$$ + $$CREATE TEMP TABLE test_table_2_id AS SELECT * FROM object_reference.object__getsert('table', 'object_group_test_table_2', object_group_name := 'object reference test group')$$ , 'Register test table 2' ); -- Drop tests SELECT throws_ok( - $$ALTER TABLE test_table_1 DROP COLUMN col1$$ + $$ALTER TABLE object_group_test_table_1 DROP COLUMN col1$$ , '23503' , NULL -- current error is crap anyway , 'Dropping col1 fails' ); SELECT throws_ok( - $$DROP TABLE test_table_2$$ + $$DROP TABLE object_group_test_table_2$$ , '23503' , NULL -- current error is crap anyway , 'Dropping test_table_2 fails' @@ -144,7 +146,7 @@ SELECT throws_ok( , 'Removing test group fails' ); SELECT lives_ok( - $$ALTER TABLE test_table_1 DROP COLUMN col2$$ + $$ALTER TABLE object_group_test_table_1 DROP COLUMN col2$$ , 'Dropping col2 works' ); @@ -178,7 +180,7 @@ SELECT lives_ok( , '__object__remove() for test_table_1 works' ); SELECT throws_ok( - $$DROP TABLE test_table_1$$ -- Should not work because column is still registered + $$DROP TABLE object_group_test_table_1$$ -- Should not work because column is still registered , '23503' , NULL -- current error is crap anyway , 'Dropping test_table_1 fails' @@ -211,6 +213,26 @@ SELECT lives_ok( ) , '__object__remove() for test_table_2 works' ); + +-- Test automatic cleanup via trigger +SELECT lives_ok( + $$CREATE TEMP TABLE cleanup_test_id AS SELECT * FROM object_reference.object__getsert('table', 'object_group_test_table_1', object_group_name := 'object reference test group')$$ + , 'Add test table back to group for cleanup test' +); +SELECT ok( + EXISTS(SELECT 1 FROM _object_reference.object WHERE object_id = (SELECT object__getsert FROM cleanup_test_id)) + , 'Object exists before cleanup test' +); +SELECT lives_ok( + $$DELETE FROM _object_reference.object_group__object WHERE object_id = (SELECT object__getsert FROM cleanup_test_id)$$ + , 'Remove from group triggers automatic cleanup attempt' +); +-- Object should be deleted because it's no longer in any group and trigger calls cleanup +SELECT ok( + NOT EXISTS(SELECT 1 FROM _object_reference.object WHERE object_id = (SELECT object__getsert FROM cleanup_test_id)) + , 'Object was automatically cleaned up after group removal' +); + SELECT lives_ok( $$SELECT object_reference.object_group__remove('object reference test group')$$ , 'Removing empty group works' diff --git a/test/sql/zzz_build.sql b/test/sql/zzz_build.sql index 4da65b4..4fc0628 100644 --- a/test/sql/zzz_build.sql +++ b/test/sql/zzz_build.sql @@ -6,7 +6,6 @@ -- Loads deps, but not extension itself \i test/pgxntool/setup.sql -CREATE EXTENSION IF NOT EXISTS count_nulls; CREATE EXTENSION IF NOT EXISTS cat_tools; CREATE SCHEMA object_reference;