diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d3779d3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,159 @@ +name: CI +on: + push: + branches: + - master + pull_request: +env: + PGUSER: postgres +jobs: + # Style linter (https://github.com/Postgres-Extensions/linter, vendored at + # .vendor/linter -- lint.mk is the thin local hand-off, see its comment). + # No `needs:` on anything: this is the cheapest possible check (no + # database, no container beyond a plain checkout, seconds to run), so it + # should never be waiting in a queue behind -- or racing for a runner slot + # against -- the PG matrix below. It's gated behind nothing and everything + # else of any real weight is gated behind it (see the `test` job's needs). + # Deliberately checked out WITHOUT submodules -- `make lint` is the same + # command a developer runs locally, and lint.mk self-initializes the + # submodule on first use. Using the exact same entry point here is what + # actually proves that self-init works, rather than papering over it with + # a submodules: true checkout. + lint: + name: ๐Ÿงน SQL Lint + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Lint SQL + run: make lint + + # Cheap gate that lets the test matrix below skip itself on commits that + # touch only docs. Runs on every push/pull_request unconditionally (no + # paths-ignore on the workflow itself) -- a workflow-level paths-ignore + # would skip this job too on a docs-only push, and the required + # all-checks-passed check would then never report and get stuck Pending in + # branch protection. + # + # Also derives the supported-PostgreSQL-major list the test job's matrix + # consumes, from a single pair of constants below, so adding or dropping a + # major is a one-line edit here instead of touching the matrix directly. + changes: + name: ๐Ÿ” Detect changes & derive PG matrix + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.diff.outputs.docs_only }} + supported_pg: ${{ steps.pg.outputs.supported_pg }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + # Full history needed so BASE and HEAD below are both reachable + # for `git diff`. + fetch-depth: 0 + - name: Compute per-push changed files + id: diff + run: | + # Fail-safe FIRST, before anything else runs: any early exit below + # (an unusable BASE/HEAD, a failed git diff) leaves this in place, + # so the test matrix only ever gets skipped after actually proving + # the push is docs-only. + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + fi + + echo "base=$BASE" + echo "head=$HEAD" + + # A missing HEAD, or an all-zeros BASE (a new branch's first push, + # where GitHub reports no prior commit), means no real diff can be + # computed -- leave the fail-safe in place. + if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE" "$HEAD") || exit 0 + [ -z "$CHANGED" ] && exit 0 + + DOCS_ONLY=true + while IFS= read -r f; do + if ! [[ "$f" =~ \.(md|asc)$ ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED" + + echo "changed files:" + echo "$CHANGED" + echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" + - name: Derive the supported-PostgreSQL-major list + id: pg + run: | + # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To + # add or drop a major, edit only the two constants below; the test + # job's matrix derives its version list from them. Do NOT hardcode + # a supported major directly in a job matrix. + # + # NEWEST -- highest PostgreSQL major tested. + # CURRENT_FLOOR -- oldest major supported. object_reference + # requires cat_tools at both build and runtime, + # and cat_tools's own current release declares + # PostgreSQL 12 as its build floor, so + # object_reference can't usefully claim support + # for anything older either. + NEWEST=18 + CURRENT_FLOOR=12 + + supported=$(seq "$NEWEST" -1 "$CURRENT_FLOOR") + + # Emit a JSON array for the test job's matrix to consume via + # fromJSON. + json=$(printf '%s\n' $supported | paste -sd, - | sed 's/^/[/; s/$/]/') + echo "supported_pg=$json" >> "$GITHUB_OUTPUT" + + test: + # Gated behind lint too, not just changes: lint is nearly free to run, + # so a baseline that's already broken by a style violation shouldn't + # also tie up runner slots on the much heavier PG matrix below. + # success() must be written explicitly -- GitHub only assumes success() + # as a job's default when the job has no if: at all. + needs: [changes, lint] + if: success() && needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + # Supported majors, from the single source in the changes job. + pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} + 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 + + # A single stable check name for use as a required status check in branch + # protection rules. Matrix jobs produce check names like "๐Ÿ˜ PostgreSQL 14" + # which would all need to be listed individually and updated whenever the + # matrix changes. This job passes if all others passed or were skipped + # (e.g. test, on a docs-only push), and fails if any failed or were + # cancelled. + all-checks-passed: + needs: [changes, lint, test] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all jobs passed or were skipped + run: | + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more jobs failed or were cancelled" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 999da21..96ccafc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ test/install/schedule # Misc tmp/ .DS_Store +.claude/settings.local.json # pg_tle generated files /pg_tle/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9443c64 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".vendor/linter"] + path = .vendor/linter + url = https://github.com/Postgres-Extensions/linter.git 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/.vendor/linter b/.vendor/linter new file mode 160000 index 0000000..b40aaf7 --- /dev/null +++ b/.vendor/linter @@ -0,0 +1 @@ +Subproject commit b40aaf70be8af80f048da777e551c5b790bd9e69 diff --git a/Makefile b/Makefile index 9ae665d..0ecc103 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 @@ -17,18 +17,46 @@ extra_clean += $(wildcard test/dump/*.log) dump_test: test/dump/run.sh test/helpers/object_table.sql $(wildcard test/dump/*.sql) $< -f # Force drop of databases if they exist +CAT_TOOLS_VERSION = 0.3.0 +CAT_TOOLS_BUILD_DIR = tmp/cat_tools-$(CAT_TOOLS_VERSION) +extra_clean += $(CAT_TOOLS_BUILD_DIR) + .PHONY: cat_tools 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 + # `pgxn install --unstable cat_tools` resolves to the newest release + # published to the PGXN package index, which is still 0.2.1 -- it fails + # standalone on modern PostgreSQL with "column oid specified more than + # once" at CREATE EXTENSION. A fixed release, 0.3.0, is tagged in + # cat_tools' own git repo but hasn't been uploaded to PGXN yet, so build + # it from that tag directly until PGXN has it. + rm -rf $(CAT_TOOLS_BUILD_DIR) + git clone --branch $(CAT_TOOLS_VERSION) --depth 1 https://github.com/Postgres-Extensions/cat_tools.git $(CAT_TOOLS_BUILD_DIR) + $(MAKE) -C $(CAT_TOOLS_BUILD_DIR) install PG_CONFIG=$(PG_CONFIG) DESTDIR=$(DESTDIR) + rm -rf $(CAT_TOOLS_BUILD_DIR) .PHONY: test_factory test_factory: $(DESTDIR)$(datadir)/extension/test_factory.control $(DESTDIR)$(datadir)/extension/test_factory.control: pgxn install test_factory + +# Style linter (see https://github.com/Postgres-Extensions/linter, vendored +# at .vendor/linter -- lint.mk is the thin local hand-off, see its comment). +# Scoped to sql/object_reference.sql rather than the default `sql/ test/`: +# the versioned install/update files under sql/ (object_reference--*.sql, +# e.g. object_reference--0.1.0.sql/--stable.sql) are frozen once released and +# never hand-edited again (see this repo's CLAUDE.md / memory), so linting +# them would produce permanent, unfixable findings and make `make lint` +# unusable as a CI gate. +# +# Guarded on .git being present: a tarball build (PGXN distribution, or any +# `git archive` checkout with no .git) has no submodule to initialize, and +# Make resolves every `include` before running any target regardless of +# which target was requested -- so an unguarded self-init rule in lint.mk +# would break `make`/`make install` entirely for a tarball build, not just +# `make lint`. +ifneq ($(wildcard .git),) +LINT_TARGETS = sql/object_reference.sql test/ +include lint.mk +endif diff --git a/lint.mk b/lint.mk new file mode 100644 index 0000000..0d18abf --- /dev/null +++ b/lint.mk @@ -0,0 +1,11 @@ +# lint.mk โ€” thin wrapper; the whole local footprint for consuming +# https://github.com/Postgres-Extensions/linter. Everything else lives in +# the .vendor/linter submodule; see its README for available targets/rules. +# +# Self-initializing (via the rule below) so `make lint` works right after a +# plain `git clone`, with no --recurse-submodules needed, and so CI can rely +# on the exact same entry point a developer would use locally. +.vendor/linter/lint.mk: + git submodule update --init -- .vendor/linter + +include .vendor/linter/lint.mk diff --git a/object_reference.control b/object_reference.control index 5c7d974..142d0b1 100644 --- a/object_reference.control +++ b/object_reference.control @@ -2,4 +2,4 @@ comment = 'Provides reference IDs for database objects' default_version = 'stable' relocatable = false schema = 'object_reference' -requires = 'cat_tools, count_nulls' +requires = 'cat_tools' 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--stable.sql b/sql/object_reference--stable.sql index 4a68ebc..dcb0026 100644 --- a/sql/object_reference--stable.sql +++ b/sql/object_reference--stable.sql @@ -3,18 +3,7 @@ \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 -$$; -/* +/* EXCLUDED CODE: schema-restriction check below not currently enforced DO $$ DECLARE c_schema CONSTANT name := (SELECT extnamespace::regnamespace::text FROM pg_extension WHERE extname = 'cat_tools'); @@ -85,7 +74,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( @@ -180,7 +169,7 @@ CREATE TABLE _object_reference.object( , 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 + /* EXCLUDED CODE: 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 ) @@ -192,58 +181,19 @@ 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 - /* TODO: needs to be a trigger + , classid oid NOT NULL + /* EXCLUDED CODE: 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 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' @@ -303,13 +253,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 @@ -325,13 +268,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 @@ -363,26 +299,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! * @@ -523,7 +442,12 @@ SELECT __object_reference.create_function( , $body$ SELECT cat_tools.objects__shared() || cat_tools.objects__address_unsupported() - || '{event trigger}' + /* + * pg_get_object_address() doesn't recognize "partitioned table" or + * "partitioned index" (only the base "table"/"index" types it derives + * from), so object identity tracking can't round-trip them. + */ + || '{event trigger, partitioned table, partitioned index}' $body$ , 'Returns array of object types that are not supported.' , 'object_reference__usage' @@ -627,6 +551,24 @@ 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' @@ -831,6 +773,69 @@ $body$ , '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 */ @@ -850,6 +855,7 @@ DECLARE r_object_v _object_reference._object_v; r_address record; + r_identity record; did_insert boolean := false; @@ -878,6 +884,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 * @@ -1254,7 +1269,7 @@ BEGIN RETURN c_next_level; EXCEPTION WHEN undefined_table THEN - /* + /* EXCLUDED CODE CREATE TEMP TABLE __object_reference__ddl_capture AS SELECT c_next_level, capture__start.object_group_id ; diff --git a/sql/object_reference.sql b/sql/object_reference.sql index e83b461..61774b0 100644 --- a/sql/object_reference.sql +++ b/sql/object_reference.sql @@ -2,18 +2,7 @@ \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 -$$; -/* +/* EXCLUDED CODE: schema-restriction check below not currently enforced DO $$ DECLARE c_schema CONSTANT name := (SELECT extnamespace::regnamespace::text FROM pg_extension WHERE extname = 'cat_tools'); @@ -84,7 +73,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( @@ -179,7 +168,7 @@ CREATE TABLE _object_reference.object( , 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 + /* EXCLUDED CODE: 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 ) @@ -191,58 +180,19 @@ 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 - /* TODO: needs to be a trigger + , classid oid NOT NULL + /* EXCLUDED CODE: 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 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 +252,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 +267,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 +298,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 +441,12 @@ SELECT __object_reference.create_function( , $body$ SELECT cat_tools.objects__shared() || cat_tools.objects__address_unsupported() - || '{event trigger}' + /* + * pg_get_object_address() doesn't recognize "partitioned table" or + * "partitioned index" (only the base "table"/"index" types it derives + * from), so object identity tracking can't round-trip them. + */ + || '{event trigger, partitioned table, partitioned index}' $body$ , 'Returns array of object types that are not supported.' , 'object_reference__usage' @@ -626,6 +550,24 @@ 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' @@ -830,6 +772,69 @@ $body$ , '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 +854,7 @@ DECLARE r_object_v _object_reference._object_v; r_address record; + r_identity record; did_insert boolean := false; @@ -877,6 +883,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 * @@ -1253,7 +1268,7 @@ BEGIN RETURN c_next_level; EXCEPTION WHEN undefined_table THEN - /* + /* EXCLUDED CODE CREATE TEMP TABLE __object_reference__ddl_capture AS SELECT c_next_level, capture__start.object_group_id ; diff --git a/test/deps.sql b/test/deps.sql index e1a53c8..300c7a8 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -2,8 +2,6 @@ -- Add any test dependency statements here -/* - * Normally these should be loaded by the cascade! -CREATE EXTENSION IF NOT EXISTS count_nulls; +/* EXCLUDED CODE: normally these should be loaded by the cascade! 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..99e1332 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:176: 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:177: 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:323: NOTICE: type reference _object_reference._object_oid.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.classid%TYPE converted to oid +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.classid%TYPE converted to oid +psql:test/temp_load.not_sql:323: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:323: 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:431: 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:543: 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:550: 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:595: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:595: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:620: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:620: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:635: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:635: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:662: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:662: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:678: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:678: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:695: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:695: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:695: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:695: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:729: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:729: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:729: NOTICE: type reference _object_reference.object_group__object.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:729: NOTICE: type reference _object_reference.object_group__object.object_id%TYPE converted to integer +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference.object.object_type%TYPE converted to cat_tools.object_type +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference._object_oid.objid%TYPE converted to oid +psql:test/temp_load.not_sql:967: NOTICE: type reference _object_reference._object_oid.objsubid%TYPE converted to integer +psql:test/temp_load.not_sql:1194: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1194: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1208: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1208: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1235: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1249: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying + +psql:test/temp_load.not_sql:1296: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1296: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer + +psql:test/temp_load.not_sql:1310: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1310: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying + +psql:test/temp_load.not_sql:1355: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer +psql:test/temp_load.not_sql:1355: NOTICE: type reference _object_reference.object_group.object_group_id%TYPE converted to integer + +psql:test/temp_load.not_sql:1369: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying +psql:test/temp_load.not_sql:1369: NOTICE: type reference _object_reference.object_group.object_group_name%TYPE converted to character varying diff --git a/test/helpers/object_table.sql b/test/helpers/object_table.sql index 67f07ee..ca90234 100644 --- a/test/helpers/object_table.sql +++ b/test/helpers/object_table.sql @@ -144,6 +144,7 @@ INSERT INTO test_prereq VALUES ; -- \N is null character +-- sql-lint:disable-block prefer-short-type: secondary column mirrors pg_catalog's own type display name (format_type), not a style choice COPY test_object(object_type, object_name, secondary, create_command, drop_command) FROM STDIN (DELIMITER '|'); table|test table||%("test column" int)| index|test table test index||%ON "test table"("test column")| @@ -159,8 +160,9 @@ cast|test type|integer|CREATE CAST ("test type" AS int4) WITH INOUT|DROP CAST (" default value|test table|test column|ALTER TABLE "test table" ALTER "test column" SET DEFAULT 0|ALTER TABLE "test table" ALTER "test column" DROP DEFAULT trigger|test table|test trigger|CREATE TRIGGER "test trigger" AFTER INSERT ON "test table" FOR EACH ROW EXECUTE PROCEDURE tg_null()|DROP TRIGGER "test trigger" ON "test table" \. +-- sql-lint:enable-block -/* Not supported +/* EXCLUDED CODE: Not supported composite type|test complex type||CREATE TYPE "test complex type" AS(r real, i real)|DROP TYPE "test complex type" view column|test view|test column|\N|\N materialized view column|test materialized view 2|test materialized view column|CREATE MATERIALIZED VIEW "test materialized view 2" AS SELECT (1,2)::"test complex type" AS "test materialized view column"|DROP MATERIALIZED VIEW "test materialized view 2" 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..10f48f7 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/capture.sql b/test/sql/capture.sql index 8eb1414..073f3f3 100644 --- a/test/sql/capture.sql +++ b/test/sql/capture.sql @@ -134,7 +134,7 @@ SELECT bag_eq( , $$SELECT object_id FROM obj_ref$$ , 'Verify captured object IDs match' ); -/* +/* EXCLUDED CODE SELECT * FROM og_o; SELECT * FROM _object_reference.object;-- WHERE object_id IN(6,9); */ diff --git a/test/sql/event_trigger.sql b/test/sql/event_trigger.sql index 2115dc7..402a5b4 100644 --- a/test/sql/event_trigger.sql +++ b/test/sql/event_trigger.sql @@ -143,7 +143,7 @@ $body$; /* - *Rename column + * Rename column */ SELECT lives_ok( $$ALTER TABLE table_under_test RENAME column_test TO test_column2$$ diff --git a/test/sql/object_group.sql b/test/sql/object_group.sql index 6304892..cd957df 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' ); @@ -82,7 +84,7 @@ SELECT is( ); -- __object__add -/* TODO +/* EXCLUDED CODE: TODO SELECT pg_temp.bogus_group( format( $$SELECT object_reference.object_group__object__add(%%s, %s)$$ @@ -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 +/* EXCLUDED CODE: 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;