From d84f3df67f6b480242d4311f791d7228851c6521 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 17 Jul 2026 18:57:10 -0500 Subject: [PATCH 1/2] cat_tools 0.2.3: fix backwards relkind mapping (#36) `relation__kind()` (relkind -> type) and `relation__relkind()` (type -> relkind), as shipped through **0.2.2**, map the `pg_class.relkind` codes `c`, `f`, and `m` **backwards**. Release a patch that fixes that. --- CLAUDE.md | 17 ++++-- HISTORY.asc | 8 +++ META.in.json | 4 +- META.json | 4 +- RELEASE.md | 87 ++++++++++++++++++++++++++++++ cat_tools.control | 2 +- control.mk | 2 +- sql/cat_tools--0.2.2--0.2.3.sql.in | 34 ++++++++++++ sql/cat_tools.sql.in | 14 ++--- test/expected/relation_type.out | 6 +-- test/sql/relation_type.sql | 25 +++++---- 11 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 RELEASE.md create mode 100644 sql/cat_tools--0.2.2--0.2.3.sql.in diff --git a/CLAUDE.md b/CLAUDE.md index 05e77e2..5f07e9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,10 +43,19 @@ Rules for what to track in git: 0. If a `.sql.in` file exists, track the `.sql.in` and **not** the corresponding `.sql`. 1. If no `.sql.in` exists, track the `.sql` directly (e.g. historical pre-0.2.0 files). -2. Version-specific install scripts (e.g. `sql/cat_tools--0.2.2.sql.in`) MUST be tracked. -3. Update scripts (e.g. `sql/cat_tools--0.2.1--0.2.2.sql.in`) MUST be tracked. -4. The current version'''s install script (e.g. `sql/cat_tools--0.2.2.sql.in`) is generated - by `make` from `sql/cat_tools.sql.in`, but MUST still be tracked (rule 2 applies). +2. Version-specific install scripts (e.g. `sql/cat_tools--0.2.2.sql.in`) are tracked BY + DEFAULT. They enable update testing (install an old version, `ALTER EXTENSION UPDATE`, + verify) and, because a new MAJOR PostgreSQL version can unpredictably break installing + an OLDER extension version, keeping old versions committed lets CI catch when a version + stops installing on a newer PG. + See https://github.com/Postgres-Extensions/pgxntool/issues/51. +3. Update scripts (e.g. `sql/cat_tools--0.2.1--0.2.2.sql.in`) MUST be tracked — they are + essential to the update path and have no substitute. +4. EXCEPTION to rule 2: a minor version that doesn't significantly change the extension + (e.g. a small bug fix) is unlikely to cross a PG supported-version boundary, so its + generated install script adds little test-coverage value and MAY be omitted (regenerated + from `sql/cat_tools.sql.in` at build time). 0.2.3 is such a case — which is why + `sql/cat_tools--0.2.3.sql.in` is intentionally NOT tracked. 5. Version-specific files MUST NEVER be edited manually — always edit `sql/cat_tools.sql.in` and regenerate. diff --git a/HISTORY.asc b/HISTORY.asc index b25e350..a271524 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,3 +1,11 @@ +0.2.3 +----- +Fix backwards pg_class.relkind mapping (c/f/m) in relation__kind() and +relation__relkind(). Per pg_class.h the correct mapping is 'c' = composite +type, 'f' = foreign table, 'm' = materialized view; releases through 0.2.2 had +these three arms reversed. + + 0.2.2 ----- Compatibility release: fixes broken installs on PostgreSQL 11 and 12+, and diff --git a/META.in.json b/META.in.json index 932f245..297b815 100644 --- a/META.in.json +++ b/META.in.json @@ -14,7 +14,7 @@ "name": "cat_tools", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.2", + "version": "0.2.3", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Tools for interfacing with the Postgres catalog", @@ -37,7 +37,7 @@ "file": "sql/cat_tools.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.2", + "version": "0.2.3", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", diff --git a/META.json b/META.json index b1a5a17..740e817 100644 --- a/META.json +++ b/META.json @@ -14,7 +14,7 @@ "name": "cat_tools", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.2", + "version": "0.2.3", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Tools for interfacing with the Postgres catalog", @@ -37,7 +37,7 @@ "file": "sql/cat_tools.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.2", + "version": "0.2.3", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..50af74c --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,87 @@ +# Releasing cat_tools + +cat_tools builds on pgxntool (https://github.com/Postgres-Extensions/pgxntool); +the release machinery (`make tag`, `make dist`) lives in `pgxntool/base.mk`. These +steps cut a new release. + +## 1. Pre-release checks +- [ ] Open issues/PRs for this release reviewed, merged or deferred. +- [ ] CI green on all supported PostgreSQL versions (the `all-checks-passed` job on master). +- [ ] Locally: `make verify-results` passes. It depends on `test` (so it runs the suite + first, then gates on the results). `make test` alone is non-gating — pgxntool marks + `installcheck` `.IGNORE`, so it never returns non-zero on a regression; only + `verify-results` (which inspects `test/regression.diffs`) is a real gate. + +## 2. Decide the version and what to track +- [ ] Pick the new version (semantic versioning). +- [ ] **Minor change? Consider NOT committing the generated versioned install script.** + If this release makes only fairly minor changes (unlikely to cross a PostgreSQL + supported-version boundary), decide whether to omit the generated + `sql/cat_tools--.sql.in` — it has little update-test-coverage value and + omitting it keeps the repo smaller (it is regenerated from `sql/cat_tools.sql.in` + at build time). The update script (`sql/cat_tools----.sql.in`) is + ALWAYS committed. See CLAUDE.md "SQL file conventions". + +## 3. Update version + changelog + +> **⚠️ CRITICAL — you are temporarily leaving the `stable` pseudo-version.** Master's +> `default_version` normally sits at the `stable` pseudo-version so that source edits +> regenerate `cat_tools--stable.sql` and never a frozen released file. Stamping a real +> version here points the generated current-version file at `cat_tools--`. The +> moment this release is merged you **MUST** flip `default_version` back to `stable` on +> master (step 6). If you forget, the next source edit on master will regenerate — and +> corrupt — the just-released version's install file. + +- [ ] Bump `default_version` in `cat_tools.control` (bumped by hand). +- [ ] Bump the version in `META.in.json` — the source of truth is + `provides.cat_tools.version` (also update the top-level `version`); `META.json`, + `control.mk`, and `meta.mk` (which feeds `PGXNVERSION`) regenerate via `make`. +- [ ] Advance `release_status` in `META.in.json` as appropriate (unstable → testing → + stable). +- [ ] Add/finish the update script `sql/cat_tools----.sql.in`; confirm + `ALTER EXTENSION cat_tools UPDATE` from the previous version reaches the new one, + on multiple PG majors. +- [ ] Stamp `HISTORY.asc`: the top `STABLE` section accumulates user-facing changes as + PRs land; at release, rename that header to the new version number. + +## 4. Verify +- [ ] `make verify-results` green (it runs `test` first, then gates on the results). +- [ ] From a clean checkout (or `git archive` of the tag): `make && make install` + regenerates and installs cleanly and `CREATE EXTENSION cat_tools;` reports the new + version — confirms a PGXN consumer can build from the tracked sources alone. (This + mirrors what `make dist` ships, since it archives the tag: committed files only, so + any omitted generated install script is regenerated on the consumer's side.) + +## 5. Tag and distribute +- [ ] Commit the release changes; working tree must be clean — `make tag` aborts with + "Untracked changes!" on a dirty tree. +- [ ] `make tag` — creates a git tag named exactly the version, UNPREFIXED (e.g. `0.2.3`, + matching the existing `0.2.2` tag; no `v` prefix), taken from `PGXNVERSION`, and + pushes it to `origin`. It is idempotent when the tag already points at HEAD, and + errors if the tag exists on a different commit. To move an existing tag use + `make forcetag` (= `make rmtag` then `make tag`); `make rmtag` deletes the tag + locally and on `origin`. +- [ ] `make dist` — depends on `tag` (and builds the HTML docs), then + `git archive`s the tag into `../cat_tools-.zip` (parent directory). + Because it archives the tag, only committed files are included. If a `.gitattributes` + exists it must be committed, or `dist` aborts (git archive only honors + `export-ignore` for committed files). `make forcedist` = `forcetag` + `dist`. +- [ ] Upload the `../cat_tools-.zip` to PGXN (manual). + +## 6. Return master to `stable` (CRITICAL — do not skip) +- [ ] As soon as the release is merged, flip `default_version` back to the `stable` + pseudo-version on master (`cat_tools.control` + `META.in.json`), open a new top + `STABLE` section in `HISTORY.asc`, and re-seed a fresh + `sql/cat_tools----stable.sql.in` update script for the next cycle. + Leaving master stamped at the real version means the next source edit regenerates + and corrupts the released version's install file. +- [ ] `rm` any stale generated `sql/cat_tools--.sql.in` / + `cat_tools--.sql` left in the tree — once `default_version` is + `stable`, `make` no longer regenerates them, so they are one-time cleanup. + +> The persistent `stable` pseudo-version (a permanent version literally named `stable`, +> with a live `sql/cat_tools----stable.sql.in` update script that every +> source fix targets) decouples fixes from version bumps. The machinery is built into +> `sql.mk`; it lands immediately after the 0.2.3 release, so 0.2.3 itself is the last +> release cut before the scheme exists — steps 3/6 above describe the flow from the +> next release onward. diff --git a/cat_tools.control b/cat_tools.control index 6a6297a..193456e 100644 --- a/cat_tools.control +++ b/cat_tools.control @@ -1,4 +1,4 @@ comment = 'Tools for intorfacing with the catalog' -default_version = '0.2.2' +default_version = '0.2.3' relocatable = false schema = 'cat_tools' diff --git a/control.mk b/control.mk index 9ac83c2..b451f7c 100644 --- a/control.mk +++ b/control.mk @@ -1,6 +1,6 @@ EXTENSIONS += cat_tools EXTENSION_SQL_FILES += sql/cat_tools.sql -EXTENSION_cat_tools_VERSION := 0.2.2 +EXTENSION_cat_tools_VERSION := 0.2.3 EXTENSION_cat_tools_VERSION_FILE = sql/cat_tools--$(EXTENSION_cat_tools_VERSION).sql EXTENSION_VERSION_FILES += $(EXTENSION_cat_tools_VERSION_FILE) $(EXTENSION_cat_tools_VERSION_FILE): sql/cat_tools.sql cat_tools.control diff --git a/sql/cat_tools--0.2.2--0.2.3.sql.in b/sql/cat_tools--0.2.2--0.2.3.sql.in new file mode 100644 index 0000000..ce2fdfb --- /dev/null +++ b/sql/cat_tools--0.2.2--0.2.3.sql.in @@ -0,0 +1,34 @@ +/* + * Correct the c/f/m relkind mapping per pg_class.h in both functions. + * CREATE OR REPLACE preserves existing grants. + */ + +CREATE OR REPLACE FUNCTION cat_tools.relation__kind( + relkind cat_tools.relation_relkind +) RETURNS cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE AS $body$ +SELECT CASE relkind + WHEN 'r' THEN 'table' + WHEN 'i' THEN 'index' + WHEN 'S' THEN 'sequence' + WHEN 't' THEN 'toast table' + WHEN 'v' THEN 'view' + WHEN 'c' THEN 'composite type' + WHEN 'f' THEN 'foreign table' + WHEN 'm' THEN 'materialized view' +END::cat_tools.relation_type +$body$; + +CREATE OR REPLACE FUNCTION cat_tools.relation__relkind( + kind cat_tools.relation_type +) RETURNS cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE AS $body$ +SELECT CASE kind + WHEN 'table' THEN 'r' + WHEN 'index' THEN 'i' + WHEN 'sequence' THEN 'S' + WHEN 'toast table' THEN 't' + WHEN 'view' THEN 'v' + WHEN 'composite type' THEN 'c' + WHEN 'foreign table' THEN 'f' + WHEN 'materialized view' THEN 'm' +END::cat_tools.relation_relkind +$body$; diff --git a/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index 6cdbdca..49b03b6 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -646,15 +646,16 @@ SELECT __cat_tools.create_function( , 'relkind cat_tools.relation_relkind' , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' , $body$ +/* Per pg_class.h: relkind 'c' = composite type, 'f' = foreign table, 'm' = materialized view. */ SELECT CASE relkind WHEN 'r' THEN 'table' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 't' THEN 'toast table' WHEN 'v' THEN 'view' - WHEN 'c' THEN 'materialized view' - WHEN 'f' THEN 'composite type' - WHEN 'm' THEN 'foreign table' + WHEN 'c' THEN 'composite type' + WHEN 'f' THEN 'foreign table' + WHEN 'm' THEN 'materialized view' END::cat_tools.relation_type $body$ , 'cat_tools__usage' @@ -666,15 +667,16 @@ SELECT __cat_tools.create_function( , 'kind cat_tools.relation_type' , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' , $body$ +/* Mapping per pg_class.h, same as relation__kind() above. */ SELECT CASE kind WHEN 'table' THEN 'r' WHEN 'index' THEN 'i' WHEN 'sequence' THEN 'S' WHEN 'toast table' THEN 't' WHEN 'view' THEN 'v' - WHEN 'materialized view' THEN 'c' - WHEN 'composite type' THEN 'f' - WHEN 'foreign table' THEN 'm' + WHEN 'composite type' THEN 'c' + WHEN 'foreign table' THEN 'f' + WHEN 'materialized view' THEN 'm' END::cat_tools.relation_relkind $body$ , 'cat_tools__usage' diff --git a/test/expected/relation_type.out b/test/expected/relation_type.out index c8d4172..cea34b0 100644 --- a/test/expected/relation_type.out +++ b/test/expected/relation_type.out @@ -12,9 +12,9 @@ ok 9 - SELECT cat_tools.relation_relkind('index') ok 10 - SELECT cat_tools.relation_relkind('sequence') ok 11 - SELECT cat_tools.relation_relkind('toast table') ok 12 - SELECT cat_tools.relation_relkind('view') -ok 13 - SELECT cat_tools.relation_relkind('materialized view') -ok 14 - SELECT cat_tools.relation_relkind('composite type') -ok 15 - SELECT cat_tools.relation_relkind('foreign table') +ok 13 - SELECT cat_tools.relation_relkind('composite type') +ok 14 - SELECT cat_tools.relation_relkind('foreign table') +ok 15 - SELECT cat_tools.relation_relkind('materialized view') ok 16 - SELECT cat_tools.relation_type('r') ok 17 - SELECT cat_tools.relation_type('i') ok 18 - SELECT cat_tools.relation_type('S') diff --git a/test/sql/relation_type.sql b/test/sql/relation_type.sql index 69beaf1..0629ab7 100644 --- a/test/sql/relation_type.sql +++ b/test/sql/relation_type.sql @@ -5,17 +5,20 @@ -- test_role is set in test/deps.sql SET LOCAL ROLE :use_role; -CREATE TEMP VIEW kinds AS - SELECT - (cat_tools.enum_range('cat_tools.relation_type'))[gs] AS kind - , (cat_tools.enum_range('cat_tools.relation_relkind'))[gs] AS relkind - FROM generate_series( - 1 - , greatest( - array_upper(cat_tools.enum_range('cat_tools.relation_type'), 1) - , array_upper(cat_tools.enum_range('cat_tools.relation_relkind'), 1) - ) - ) gs +/* + * Canonical (relkind, kind) pairs per pg_class.h, hard-coded so the assertions + * below actually verify the mapping rather than re-encoding enum order. + */ +CREATE TEMP VIEW kinds(relkind, kind) AS + VALUES + ('r'::text, 'table'::text) + , ('i', 'index') + , ('S', 'sequence') + , ('t', 'toast table') + , ('v', 'view') + , ('c', 'composite type') + , ('f', 'foreign table') + , ('m', 'materialized view') ; SELECT plan( From d977bacd5b101acd3a992b4c484b0dd70ca7575b Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sat, 18 Jul 2026 16:14:50 -0500 Subject: [PATCH 2/2] Release cat_tools 0.2.3. Correct the backwards pg_class.relkind mapping in relation__kind() and relation__relkind(). Per pg_class.h, 'c' is a composite type, 'f' a foreign table, and 'm' a materialized view; releases through 0.2.2 had these three reversed. Fix binary pg_upgrade failing for databases that reached 0.2.2 via the 0.2.0->0.2.2 or 0.2.1->0.2.2 update scripts. Those scripts filtered omitted columns with `!= ANY` instead of `!= ALL`; on a multi-element list `!= ANY` strips nothing, so catalog columns removed in newer PostgreSQL (relhasoids, relhaspkey) remained in _cat_tools.pg_class_v and made pg_upgrade to PG12+ fail. Rather than re-issue the already-published 0.2.0->0.2.2 and 0.2.1->0.2.2 scripts, the 0.2.2->0.2.3 update repairs such a database at runtime: it detects the broken build (a relhasoids column still present on _cat_tools.pg_class_v) and only then drops and recreates the public view chain (cat_tools.pg_class_v, cat_tools.column, cat_tools.pg_class()) with the corrected column list. A fresh 0.2.2 install already has correct views, so the rebuild is skipped entirely and its views -- and any objects depending on them -- are left untouched. Because the repair is DROP+CREATE without CASCADE, a database that IS repaired must have any user objects depending on those public views dropped first and recreated afterward, or the update aborts. Consequently, to binary-pg_upgrade a cluster whose cat_tools reached 0.2.2 via those update paths to PostgreSQL 12+, first ALTER EXTENSION cat_tools UPDATE to 0.2.3 (or newer); this is documented in README.asc and HISTORY.asc. Track the generated sql/cat_tools--0.2.3.sql.in install script -- the nontrivial 0.2.2->0.2.3 update warrants committing it for update-test coverage and provenance -- and update CLAUDE.md's SQL-file-tracking rule accordingly. Also adds a RELEASE.md and a test/build/upgrade.sql regression guard proving a fresh 0.2.2 install's dependent objects survive the update to 0.2.3. Verified on PostgreSQL 12 and 17 via the new test/build/upgrade.sql (a fresh 0.2.2 install with dependent views updates to 0.2.3 with those views left in place), and by master's extension-update-test (PG10), which installs 0.2.0, updates to current (0.2.0->0.2.2->0.2.3, exercising the conditional repair on a genuinely broken build), plus the explicit 0.2.1->0.2.2 leg. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 8 + CLAUDE.md | 23 +- HISTORY.asc | 12 +- README.asc | 24 + RELEASE.md | 10 + sql/cat_tools--0.2.2--0.2.3.sql.in | 196 ++++ sql/cat_tools--0.2.3.sql.in | 1542 ++++++++++++++++++++++++++++ test/build/expected/upgrade.out | 1 + test/build/upgrade.sql | 39 + 9 files changed, 1846 insertions(+), 9 deletions(-) create mode 100644 sql/cat_tools--0.2.3.sql.in create mode 100644 test/build/expected/upgrade.out create mode 100644 test/build/upgrade.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e245f9f..703a984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,14 @@ jobs: if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1 fi + # Verify the 0.2.2->0.2.3 view rebuild actually FIRED (not just ran). On + # PG10 the 0.2.0->0.2.2 path leaves _cat_tools.pg_class_v carrying + # relhasoids (the buggy `!= ANY` no-op never stripped it), and the + # conditional 0.2.2->0.2.3 repair must detect that and rebuild the view + # without it. This is the REAL broken path: on PG12+ relhasoids never + # existed, so only PG10 exercises the rebuild. ON_ERROR_STOP makes the + # RAISE EXCEPTION fail the step. + psql -v ON_ERROR_STOP=1 -c "DO \$\$ BEGIN IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = '_cat_tools.pg_class_v'::regclass AND attname = 'relhasoids' AND NOT attisdropped AND attnum > 0) THEN RAISE EXCEPTION 'pg_class_v still exposes relhasoids after update to current -- 0.2.2->0.2.3 rebuild did not fire'; END IF; END \$\$" - name: Test update 0.2.1 → 0.2.2 run: | # PG takes the shortest update path, so the 0.2.0->current test above goes diff --git a/CLAUDE.md b/CLAUDE.md index 5f07e9d..93f2fd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,23 @@ Never repeat the same comment verbatim in adjacent code — write it once and re **Always open PRs against the main repo** (`Postgres-Extensions/cat_tools`), not a fork. +## Pull request descriptions + +The maintainer builds the squash commit message from the PR description, so the +**opening** of the description is used directly as the basis for that commit message. +Write it accordingly: + +- Make the opening stand alone as a good commit message: no leading header/title line + (the PR title is the subject), concise, self-contained. Multiple paragraphs are fine. +- Lead with the substantive change and why. Keep incidental changes (minor doc tweaks, + dependency/action version bumps, small cleanups) OUT of the opening — put them lower + or omit them; the diff carries those details for anyone who wants them. +- Do NOT add a marker delimiting the "commit message" from "the rest". Just let the + opening carry its own weight, with any extra context following after it. +- Do NOT hard-wrap paragraphs: write each paragraph as a single long line (blank line + between paragraphs). Hard-wrapping at ~80 columns conflicts with how GitHub builds the + squash commit message from the description. + ## SQL file conventions Rules for what to track in git: @@ -51,11 +68,7 @@ Rules for what to track in git: See https://github.com/Postgres-Extensions/pgxntool/issues/51. 3. Update scripts (e.g. `sql/cat_tools--0.2.1--0.2.2.sql.in`) MUST be tracked — they are essential to the update path and have no substitute. -4. EXCEPTION to rule 2: a minor version that doesn't significantly change the extension - (e.g. a small bug fix) is unlikely to cross a PG supported-version boundary, so its - generated install script adds little test-coverage value and MAY be omitted (regenerated - from `sql/cat_tools.sql.in` at build time). 0.2.3 is such a case — which is why - `sql/cat_tools--0.2.3.sql.in` is intentionally NOT tracked. +4. EXCEPTION to rule 2: a version that changes little AND ships no nontrivial update-path machinery MAY omit its generated install script (little test-coverage value; it is regenerated from `sql/cat_tools.sql.in` at build time). Track it whenever the version carries meaningful changes or a nontrivial update script — a complex update warrants the committed install script for update-test coverage and provenance. 5. Version-specific files MUST NEVER be edited manually — always edit `sql/cat_tools.sql.in` and regenerate. diff --git a/HISTORY.asc b/HISTORY.asc index a271524..28a544b 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,9 +1,13 @@ 0.2.3 ----- -Fix backwards pg_class.relkind mapping (c/f/m) in relation__kind() and -relation__relkind(). Per pg_class.h the correct mapping is 'c' = composite -type, 'f' = foreign table, 'm' = materialized view; releases through 0.2.2 had -these three arms reversed. +Fix the backwards `pg_class.relkind` mapping (c/f/m) in `relation__kind()` and +`relation__relkind()`. Also fix binary `pg_upgrade` for databases updated from +0.2.0/0.2.1, whose catalog views kept columns removed in PostgreSQL 12+ +(`relhasoids`/`relhaspkey`): to `pg_upgrade` such a cluster to PG12+, first +update the extension to 0.2.3, which rebuilds those views. The rebuild drops and +recreates public views (`pg_class_v`, `column`, `pg_class()`), so drop any of +your own dependent objects first. See the README "Updating the extension" +section and https://github.com/Postgres-Extensions/cat_tools/pull/42. 0.2.2 diff --git a/README.asc b/README.asc index 6d2657d..c11811a 100644 --- a/README.asc +++ b/README.asc @@ -25,6 +25,30 @@ This is very much a work in progress. If it doesn't do something you need, pleas Works on Postgres 9.3 and above. +== Updating the extension + +Usually just run `ALTER EXTENSION cat_tools UPDATE`. Two special cases apply to a +database that reached 0.2.2 via the *0.2.0/0.2.1 update scripts*, whose catalog +views are broken (they still reference `pg_class`/`pg_attribute` columns — +`relhasoids`, `relhaspkey` — that a fresh 0.2.2 install correctly omits). A fresh +0.2.2 install has correct views and is unaffected by either case. + +=== Binary `pg_upgrade` to PostgreSQL 12+ from such a database + +Update the extension to 0.2.3 (or newer) on the *old* cluster first, then run +binary `pg_upgrade`. The broken views reference catalog columns removed in PG12+, +so `pg_upgrade` fails otherwise; the 0.2.2→0.2.3 update repairs them first. See +the 0.2.3 entry in HISTORY.asc for details. + +=== Updating such a database to 0.2.3 rebuilds some public views + +*Only* when the views are broken, the 0.2.2→0.2.3 update `DROP`+`CREATE`s (without +`CASCADE`) these public objects: `cat_tools.pg_class_v`, `cat_tools.column`, and +the `cat_tools.pg_class(regclass)` function. If you created your own objects +depending on any of these, drop them before updating and recreate them afterward; +the update aborts otherwise. A fresh 0.2.2 install's views are left untouched, so +its dependents are unaffected. See the 0.2.3 entry in HISTORY.asc for details. + == Data Types * `object_type` - Descriptive names for every type of Postgres object (table, operator, rule, etc) diff --git a/RELEASE.md b/RELEASE.md index 50af74c..8cad584 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -85,3 +85,13 @@ steps cut a new release. > `sql.mk`; it lands immediately after the 0.2.3 release, so 0.2.3 itself is the last > release cut before the scheme exists — steps 3/6 above describe the flow from the > next release onward. + +## Notes and caveats + +- **0.2.3 catalog-view repair / `pg_upgrade` caveat.** Databases updated from + 0.2.0/0.2.1 hold broken catalog views that fail binary `pg_upgrade` to PostgreSQL + 12+ until the extension is updated to 0.2.3 (the update rebuilds them, dropping and + recreating the public `pg_class_v`/`column`/`pg_class()` objects without `CASCADE`). + Surface this in the release notes when people may cross the PG 11 → 12+ boundary. See + README.asc "Updating the extension" and + https://github.com/Postgres-Extensions/cat_tools/pull/42. diff --git a/sql/cat_tools--0.2.2--0.2.3.sql.in b/sql/cat_tools--0.2.2--0.2.3.sql.in index ce2fdfb..4954cff 100644 --- a/sql/cat_tools--0.2.2--0.2.3.sql.in +++ b/sql/cat_tools--0.2.2--0.2.3.sql.in @@ -1,3 +1,199 @@ +/* + * NOTE: All pg_temp objects must be dropped at the end of the script! + * Otherwise the eventual DROP CASCADE of pg_temp when the session ends will + * also drop the extension! Instead of risking problems, create our own + * "temporary" schema instead. + */ +CREATE SCHEMA __cat_tools; + +CREATE FUNCTION __cat_tools.omit_column( + rel text + , omit name[] DEFAULT array['oid'] +) RETURNS text LANGUAGE sql STABLE AS $body$ +SELECT array_to_string(array( + SELECT attname + FROM pg_attribute a + WHERE attrelid = rel::regclass + AND NOT attisdropped + AND attnum >= 0 + AND attname != ALL( omit ) + ORDER BY attnum + ) + , ', ' +) +$body$; + +/* + * CONDITIONAL REPAIR: rebuild the _cat_tools.pg_class_v dependency chain ONLY + * when it is actually broken, leaving a correct chain (and any user objects that + * depend on the public views) untouched. + * + * Background: the published 0.2.2 could be reached two ways. + * - A FRESH 0.2.2 install builds the views with the fixed omit_column + * (`!= ALL`), which correctly strips relhasoids/relhaspkey from + * _cat_tools.pg_class_v. These views are already correct. + * - The 0.2.0->0.2.2 / 0.2.1->0.2.2 update scripts used the BUGGY omit_column + * (`!= ANY`, a no-op for a multi-element omit list), so relhasoids/relhaspkey + * were never stripped. Once such a database is binary-pg_upgraded to PG12+ + * (where pg_class has no relhasoids/relhaspkey), the view references + * non-existent catalog columns and is broken. + * + * Fingerprint: a column literally named relhasoids on _cat_tools.pg_class_v means + * this is a buggy-update-path build that still needs repair. This single signal + * is sufficient: a broken-via-update database can only exist on PG<=10 (that is + * the only major where relhasoids exists to be captured), so a 0.2.2 database on + * PG12+ is necessarily a fresh, correct install and MUST be left alone. + * + * When the fingerprint is absent we do NOTHING to the views. The old + * unconditional DROP+CREATE broke anyone with a hard dependency on the public + * views (cat_tools.pg_class_v, cat_tools.column, cat_tools.pg_class()) even on a + * fresh, correct install; guarding it fixes that. + * + * The drops and recreates live inside a DO block: DROP VIEW/FUNCTION cannot sit + * inside a plain IF, so they run as EXECUTEd dynamic SQL. CREATE OR REPLACE VIEW + * cannot drop columns, so the chain is dropped in reverse dependency order (no + * CASCADE, so a DROP fails loudly on a user dependency) and recreated forward + * with the corrected column list. cat_tools.pg_class() is dropped because it + * RETURNS the cat_tools.pg_class_v rowtype. + */ +DO $do$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_attribute + WHERE attrelid = '_cat_tools.pg_class_v'::regclass + AND attname = 'relhasoids' + AND NOT attisdropped + AND attnum > 0 + ) THEN + RETURN; + END IF; + + EXECUTE 'DROP VIEW cat_tools.column'; + EXECUTE 'DROP VIEW _cat_tools.column'; + EXECUTE 'DROP VIEW _cat_tools.pg_attribute_v'; + EXECUTE 'DROP FUNCTION cat_tools.pg_class(pg_catalog.regclass)'; + EXECUTE 'DROP VIEW cat_tools.pg_class_v'; + EXECUTE 'DROP VIEW _cat_tools.pg_class_v'; + + EXECUTE format($fmt$ +CREATE VIEW _cat_tools.pg_class_v AS + SELECT c.oid AS reloid + , %s + , n.nspname AS relschema + FROM pg_class c + LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_class', array['oid', 'relhasoids', 'relhaspkey']) + ); + EXECUTE 'REVOKE ALL ON _cat_tools.pg_class_v FROM public'; + + EXECUTE $sql$ +CREATE VIEW cat_tools.pg_class_v AS + SELECT * + FROM _cat_tools.pg_class_v + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND relkind IN( 'r', 'v', 'f' ) +; +$sql$; + EXECUTE 'GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage'; + + EXECUTE $sql$ +CREATE FUNCTION cat_tools.pg_class( + rel pg_catalog.regclass +) RETURNS cat_tools.pg_class_v LANGUAGE sql STABLE AS $body$ +SELECT * FROM cat_tools.pg_class_v WHERE reloid = $1 +$body$; +$sql$; + EXECUTE 'REVOKE ALL ON FUNCTION cat_tools.pg_class(pg_catalog.regclass) FROM public'; + EXECUTE 'GRANT EXECUTE ON FUNCTION cat_tools.pg_class(pg_catalog.regclass) TO cat_tools__usage'; + + /* + * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). + * Include it cast to text[] on PG11+; expose as NULL::text[] on older versions. + */ + EXECUTE format($fmt$ +CREATE VIEW _cat_tools.pg_attribute_v AS + SELECT %s + , c.* + , t.oid AS typoid + , %s + , a.attmissingval::text::text[] AS attmissingval -- SED: REQUIRES 11! + , NULL::text[] AS attmissingval -- SED: PRIOR TO 11! + FROM pg_attribute a + LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) + LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) +; +$fmt$ + /* + * attmissingval is explicitly included above (cast to text[] via SED markers). + * Omit it here so it doesn't appear twice, and omit oid to avoid conflicts on PG12+. + */ + , __cat_tools.omit_column('pg_catalog.pg_attribute', array['oid', 'attmissingval', 'attcacheoff']) + , __cat_tools.omit_column('pg_catalog.pg_type') + ); + EXECUTE 'REVOKE ALL ON _cat_tools.pg_attribute_v FROM public'; + + /* + * Rebuild _cat_tools.column with computed columns first and all pg_attribute_v + * columns (via %s) last. This ordering lets future CREATE OR REPLACE VIEW + * upgrades extend the pg_attribute_v column list at the end without disturbing + * existing column positions. See issue 13 for restoring logical ordering. + */ + EXECUTE format($fmt$ +CREATE VIEW _cat_tools.column AS + SELECT pg_catalog.format_type(typoid, atttypmod) AS column_type + , CASE typtype + WHEN 'd' THEN pg_catalog.format_type(typbasetype, typtypmod) + WHEN 'e' THEN 'text' + ELSE pg_catalog.format_type(typoid, atttypmod) + END AS base_type + , pk.conkey AS pk_columns + , ARRAY[attnum] <@ pk.conkey AS is_pk_member + , (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) + FROM pg_catalog.pg_attrdef d + WHERE d.adrelid = a.attrelid + AND d.adnum = a.attnum + AND a.atthasdef + ) AS column_default + , %s + FROM _cat_tools.pg_attribute_v a + LEFT JOIN pg_constraint pk + ON ( reloid = pk.conrelid ) + AND pk.contype = 'p' +; +$fmt$ + , __cat_tools.omit_column('_cat_tools.pg_attribute_v') + ); + EXECUTE 'REVOKE ALL ON _cat_tools.column FROM public'; + + EXECUTE $sql$ +CREATE VIEW cat_tools.column AS + SELECT * + FROM _cat_tools.column + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND attnum > 0 + AND NOT attisdropped + AND relkind IN( 'r', 'v', 'f' ) + AND ( + pg_has_role(SESSION_USER, relowner, 'USAGE'::text) + OR has_column_privilege(SESSION_USER, reloid, attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY relschema, relname, attnum +; +$sql$; + EXECUTE 'GRANT SELECT ON cat_tools.column TO cat_tools__usage'; +END +$do$; + +-- Drop temporary helper objects +DROP FUNCTION __cat_tools.omit_column( + rel text + , omit name[] +); +DROP SCHEMA __cat_tools; + /* * Correct the c/f/m relkind mapping per pg_class.h in both functions. * CREATE OR REPLACE preserves existing grants. diff --git a/sql/cat_tools--0.2.3.sql.in b/sql/cat_tools--0.2.3.sql.in new file mode 100644 index 0000000..49b03b6 --- /dev/null +++ b/sql/cat_tools--0.2.3.sql.in @@ -0,0 +1,1542 @@ +@generated@ + +SET LOCAL client_min_messages = WARNING; + +DO $$ +BEGIN + CREATE ROLE cat_tools__usage NOLOGIN; +EXCEPTION WHEN duplicate_object THEN + NULL; +END +$$; + +/* + * NOTE: All pg_temp objects must be dropped at the end of the script! + * Otherwise the eventual DROP CASCADE of pg_temp when the session ends will + * also drop the extension! Instead of risking problems, create our own + * "temporary" schema instead. + */ +CREATE SCHEMA __cat_tools; + +-- Schema already created via CREATE EXTENSION +GRANT USAGE ON SCHEMA cat_tools TO cat_tools__usage; +ALTER DEFAULT PRIVILEGES IN SCHEMA cat_tools GRANT USAGE ON TYPES TO cat_tools__usage; +CREATE SCHEMA _cat_tools; + +@generated@ + +CREATE FUNCTION __cat_tools.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +-- See also test/setup.sql +CREATE FUNCTION __cat_tools.omit_column( + rel text + , omit name[] DEFAULT array['oid'] +) RETURNS text LANGUAGE sql STABLE AS $body$ +SELECT array_to_string(array( + SELECT attname + FROM pg_attribute a + WHERE attrelid = rel::regclass + AND NOT attisdropped + AND attnum >= 0 + AND attname != ALL( omit ) + ORDER BY attnum + ) + , ', ' +) +$body$; + +@generated@ + +/* + * Starting in PG12 oid columns in catalog tables are no longer hidden, so we + * need a way to include all the fields in a table *except* for the OID column. + */ +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS + SELECT c.oid AS reloid + , %s + , n.nspname AS relschema + FROM pg_class c + LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_class', array['oid', 'relhasoids', 'relhaspkey']) +)); +REVOKE ALL ON _cat_tools.pg_class_v FROM public; + +/* + * Temporary stub function. We do this so we can use the nice create_function + * function that we're about to create to create the real version of this + * function. + */ +CREATE FUNCTION cat_tools.function__arg_types_text(text +) RETURNS text LANGUAGE sql AS 'SELECT $1'; + +CREATE FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text DEFAULT NULL + , comment text DEFAULT NULL +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); + + create_template CONSTANT text := $template$ +CREATE OR REPLACE FUNCTION %s( +%s +) RETURNS %s AS +%L +$template$ + ; + + revoke_template CONSTANT text := $template$ +REVOKE ALL ON FUNCTION %s( +%s +) FROM public; +$template$ + ; + + grant_template CONSTANT text := $template$ +GRANT EXECUTE ON FUNCTION %s( +%s +) TO %s; +$template$ + ; + + comment_template CONSTANT text := $template$ +COMMENT ON FUNCTION %s( +%s +) IS %L; +$template$ + ; + +@generated@ + +BEGIN + PERFORM __cat_tools.exec( format( + create_template + , function_name + , args + , options -- TODO: Force search_path if options ~* 'definer' + , body + ) ) + ; + PERFORM __cat_tools.exec( format( + revoke_template + , function_name + , c_simple_args + ) ) + ; + + IF grants IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + grant_template + , function_name + , c_simple_args + , grants + ) ) + ; + END IF; + + IF comment IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + comment_template + , function_name + , c_simple_args + , comment + ) ) + ; + END IF; +END +$body$; + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + input_arg_types pg_catalog.regtype[]; + + c_template CONSTANT text := $fmt$CREATE FUNCTION pg_temp.cat_tools__function__arg_types__temp_function( + %s + ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN NULL; END' + $fmt$; + + temp_proc pg_catalog.regprocedure; + sql text; +BEGIN + sql := format( + c_template + , arguments + , 'void' + ); + --RAISE DEBUG 'Executing SQL %', sql; + DECLARE + v_type pg_catalog.regtype; + BEGIN + EXECUTE sql; + EXCEPTION WHEN invalid_function_definition THEN + v_type := (regexp_matches( SQLERRM, 'function result type must be ([^ ]+) because of' ))[1]; + sql := format( + c_template + , arguments + , v_type + ); + EXECUTE sql; + END; + + /* + * Get new OID. *This must be done dynamically!* Otherwise we get stuck + * with a CONST oid after first compilation. The regproc cast ensures there's + * only one function with this name. The cast to regprocedure is for the sake + * of the DROP down below. + */ + EXECUTE $$SELECT 'pg_temp.cat_tools__function__arg_types__temp_function'::pg_catalog.regproc::pg_catalog.regprocedure$$ INTO temp_proc; + SELECT INTO STRICT input_arg_types + -- This is here to re-cast the array as 1-based instead of 0 based (better solutions welcome!) + string_to_array(proargtypes::text,' ')::pg_catalog.regtype[] + FROM pg_proc + WHERE oid = temp_proc + ; + -- NOTE: DROP may not accept all the argument options that CREATE does, so use temp_proc + EXECUTE format( + $fmt$DROP FUNCTION %s$fmt$ + , temp_proc + ); + + RETURN input_arg_types; +END +$body$ + , 'cat_tools__usage' + , 'Returns argument types for a function argument body as an array. Unlike a + normal regprocedure cast, this function accepts anything that is valid when + defining a function.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.function__arg_types($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns argument types for a function argument body as text. Unlike a + normal regprocedure cast, this function accepts anything that is valid when + defining a function.' + +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.regprocedure' + , $$ + function_name text + , arguments text$$ + , $$pg_catalog.regprocedure LANGUAGE sql$$ + , $body$ +SELECT format( + '%s(%s)' + , $1 + , cat_tools.function__arg_types_text($2) +)::pg_catalog.regprocedure +$body$ + , 'cat_tools__usage' + , 'Returns a regprocedure for a given function name and arguments. Unlike a + normal regprocedure cast, arguments can contain anything that is valid when + defining a function.' +); + + +@generated@ + +CREATE TYPE cat_tools.constraint_type AS ENUM( + 'domain constraint', 'table constraint' +); +COMMENT ON TYPE cat_tools.constraint_type IS $$Descriptive names for every type of Postgres object (table, operator, rule, etc)$$; +CREATE TYPE cat_tools.procedure_type AS ENUM( + 'aggregate', 'function' +); +COMMENT ON TYPE cat_tools.procedure_type IS $$Types of constraints (`domain constraint` or `table_constraint`)$$; + +CREATE TYPE cat_tools.relation_type AS ENUM( + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' +); +COMMENT ON TYPE cat_tools.relation_type IS $$Types of objects stored in `pg_class`$$; + +CREATE TYPE cat_tools.relation_relkind AS ENUM( + 'r' + , 'i' + , 'S' + , 't' + , 'v' + , 'c' + , 'f' + , 'm' +); +COMMENT ON TYPE cat_tools.relation_relkind IS $$Valid values for `pg_class.relkind`$$; + +@generated@ + +CREATE TYPE cat_tools.object_type AS ENUM( + -- pg_class + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' + /* + * NOTE! These are a bit weird because columns live in pg_attribute, but + * address stuff recognizes columns as part of pg_class with a subobjid <> 0! + */ + , 'table column' + , 'index column' + , 'sequence column' + , 'toast table column' + , 'view column' + , 'materialized view column' + , 'composite type column' + , 'foreign table column' + -- pg_constraint + -- NOTE: a domain itself is considered to be a type + , 'domain constraint', 'table constraint' + -- pg_proc + , 'aggregate', 'function' + -- This is taken from getObjectTypeDescription() in objectaddress.c in the Postgres source code + , 'type' + , 'cast' + , 'collation' + , 'conversion' + , 'default value' -- pg_attrdef + , 'language' + , 'large object' -- pg_largeobject + , 'operator' + , 'operator class' -- pg_opclass + , 'operator family' -- pg_opfamily + , 'operator of access method' -- pg_amop + , 'function of access method' -- pg_amproc + , 'rule' -- pg_rewrite + , 'trigger' + , 'schema' -- pg_namespace + , 'text search parser' -- pg_ts_parser + , 'text search dictionary' -- pg_ts_dict + , 'text search template' -- pg_ts_template + , 'text search configuration' -- pg_ts_config + , 'role' -- pg_authid + , 'database' + , 'tablespace' + , 'foreign-data wrapper' -- pg_foreign_data_wrapper + , 'server' -- pg_foreign_server + , 'user mapping' -- pg_user_mapping + , 'default acl' -- pg_default_acl + , 'extension' + , 'event trigger' -- pg_event_trigger -- SED: REQUIRES 9.3! + , 'policy' -- SED: REQUIRES 9.5! + , 'transform' -- SED: REQUIRES 9.5! + , 'access method' -- pg_am +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.objects__shared' + , '' + , 'cat_tools.object_type[] LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT '{role,database,tablespace}'::cat_tools.object_type[] +$body$ + , 'cat_tools__usage' + , 'Returns array of object types for shared objects.' +); +SELECT __cat_tools.create_function( + 'cat_tools.objects__shared_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT * FROM pg_catalog.unnest(cat_tools.objects__shared()) +$body$ + , 'cat_tools__usage' + , 'Returns set of object types for shared objects.' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_shared' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT object_type = ANY(cat_tools.objects__shared()) +$body$ + , 'cat_tools__usage' + , 'Returns true if object_type is a shared object.' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_shared' + , 'object_type text' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT cat_tools.object__is_shared(object_type::cat_tools.object_type) +$body$ + , 'cat_tools__usage' + , 'Returns true if object_type is a shared object.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.objects__address_unsupported' + , '' + , 'cat_tools.object_type[] LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT array[ + 'toast table'::cat_tools.object_type, 'composite type' + , 'index column' , 'sequence column', 'toast table column', 'view column' + , 'materialized view column', 'composite type column' +] +$body$ + , 'cat_tools__usage' + , 'Returns array of object types not supported by pg_get_object_address().' +); +SELECT __cat_tools.create_function( + 'cat_tools.objects__address_unsupported_srf' + , '' + , 'SETOF cat_tools.object_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT * FROM pg_catalog.unnest(cat_tools.objects__address_unsupported()) +$body$ + , 'cat_tools__usage' + , 'Returns set of object types not supported by pg_get_object_address().' +); +@generated@ +SELECT __cat_tools.create_function( + 'cat_tools.object__is_address_unsupported' + , 'object_type cat_tools.object_type' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT object_type = ANY(cat_tools.objects__address_unsupported()) +$body$ + , 'cat_tools__usage' + , 'Returns true if object type is not supported by pg_get_object_address().' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__is_address_unsupported' + , 'object_type text' + , 'boolean LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT cat_tools.object__is_address_unsupported(object_type::cat_tools.object_type) +$body$ + , 'cat_tools__usage' + , 'Returns true if object type is not supported by pg_get_object_address().' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.object__catalog' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT ( + 'pg_catalog.' + || CASE + WHEN object_type = ANY( array[ + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' + ]::cat_tools.object_type[] ) + THEN 'pg_class' + WHEN object_type = ANY( '{domain constraint,table constraint}'::cat_tools.object_type[] ) + THEN 'pg_constraint' + WHEN object_type = ANY( '{aggregate,function}'::cat_tools.object_type[] ) + THEN 'pg_proc' + WHEN object_type::text LIKE '% column' + THEN 'pg_attribute' + ELSE CASE object_type + -- Unusual cases + -- s/, \(.\{-}\) -- \(.*\)/ WHEN \1 THEN '\2'/ + WHEN 'default value' THEN 'pg_attrdef' + WHEN 'large object' THEN 'pg_largeobject' + WHEN 'operator class' THEN 'pg_opclass' + WHEN 'operator family' THEN 'pg_opfamily' + WHEN 'operator of access method' THEN 'pg_amop' + WHEN 'function of access method' THEN 'pg_amproc' + WHEN 'rule' THEN 'pg_rewrite' + WHEN 'schema' THEN 'pg_namespace' + WHEN 'text search parser' THEN 'pg_ts_parser' + WHEN 'text search dictionary' THEN 'pg_ts_dict' + WHEN 'text search template' THEN 'pg_ts_template' + WHEN 'text search configuration' THEN 'pg_ts_config' + WHEN 'role' THEN 'pg_authid' + WHEN 'foreign-data wrapper' THEN 'pg_foreign_data_wrapper' + WHEN 'server' THEN 'pg_foreign_server' + WHEN 'user mapping' THEN 'pg_user_mapping' + WHEN 'default acl' THEN 'pg_default_acl' + WHEN 'event trigger' THEN 'pg_event_trigger' -- SED: REQUIRES 9.3! + WHEN 'access method' THEN 'pg_am' + ELSE 'pg_' || object_type::text + END + END + )::pg_catalog.regclass +$body$ + , 'cat_tools__usage' + , 'Returns catalog table that is used to store objects' +); +@generated@ +SELECT __cat_tools.create_function( + 'cat_tools.object__catalog' + , 'object_type text' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__catalog(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns catalog table that is used to store objects' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.object__address_classid' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$ +SELECT CASE + WHEN c = 'pg_catalog.pg_attribute'::regclass THEN 'pg_catalog.pg_class'::regclass + ELSE c + END + FROM cat_tools.object__catalog(object_type::cat_tools.object_type) c +$body$ + , 'cat_tools__usage' + , 'Returns the classid used by the pg_*_object*() functions for an object_type' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__address_classid' + , 'object_type text' + , 'pg_catalog.regclass LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__address_classid(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns the classid used by the pg_*_object*() functions for an object_type' +); + +@generated@ + +CREATE TABLE _cat_tools.catalog_metadata( + object_catalog pg_catalog.regclass + CONSTRAINT catalog_metadata__pk_object_catalog PRIMARY KEY + , namespace_field name + , reg_type pg_catalog.regtype + , simple_reg_type pg_catalog.regtype +); +-- Table is populated later, after enum_range_srf is created +SELECT __cat_tools.create_function( + '_cat_tools.catalog_metadata__get' + , 'object_catalog _cat_tools.catalog_metadata.object_catalog%TYPE' + , '_cat_tools.catalog_metadata LANGUAGE plpgsql IMMUTABLE' -- Technically should be STABLE + , $body$ +DECLARE + o _cat_tools.catalog_metadata; +BEGIN + SELECT INTO STRICT o + * + FROM _cat_tools.catalog_metadata m + WHERE m.object_catalog = catalog_metadata__get.object_catalog + ; + RETURN o; +END +$body$ + , 'cat_tools__usage' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_catalog pg_catalog.regclass' + , 'pg_catalog.regtype LANGUAGE sql SECURITY DEFINER STRICT IMMUTABLE' + , $body$ +SELECT (_cat_tools.catalog_metadata__get(object_catalog)).reg_type +$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_type cat_tools.object_type' + , 'pg_catalog.regtype LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__reg_type(cat_tools.object__catalog(object_type))$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type' + , 'object_type text' + , 'pg_catalog.regtype LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.object__reg_type(object_type::cat_tools.object_type)$body$ + , 'cat_tools__usage' + , 'Returns the object identifier type (ie: regclass) associated with a system catalog (ie: pg_class).' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.object__reg_type_catalog' + , 'object_identifier_type regtype' + , 'pg_catalog.regclass LANGUAGE plpgsql SECURITY DEFINER STRICT IMMUTABLE' + , $body$ +DECLARE + cat pg_catalog.regclass; +BEGIN + SELECT INTO STRICT cat + object_catalog + FROM _cat_tools.catalog_metadata m + WHERE m.reg_type = object_identifier_type + OR m.simple_reg_type = object_identifier_type + ; + RETURN cat; +EXCEPTION WHEN no_data_found THEN + IF object_identifier_type::text LIKE 'reg%' THEN + RAISE 'object identifier type % is not supported', object_identifier_type + USING + HINT = format( 'If %I is a valid object identifier type please open an issue at https://github.com/decibel/cat_tools/issues.', object_identifier_type ) + , ERRCODE = 'feature_not_supported' + ; + ELSE + RAISE '% is not a object identifier type', object_identifier_type + USING + HINT = 'See https://www.postgresql.org/docs/current/static/datatype-oid.html' + , ERRCODE = 'wrong_object_type' + ; + END IF; +END +$body$ + , 'cat_tools__usage' + , 'Returns the system catalog that stores a particular object identifier type.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind cat_tools.relation_relkind' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* Per pg_class.h: relkind 'c' = composite type, 'f' = foreign table, 'm' = materialized view. */ +SELECT CASE relkind + WHEN 'r' THEN 'table' + WHEN 'i' THEN 'index' + WHEN 'S' THEN 'sequence' + WHEN 't' THEN 'toast table' + WHEN 'v' THEN 'view' + WHEN 'c' THEN 'composite type' + WHEN 'f' THEN 'foreign table' + WHEN 'm' THEN 'materialized view' +END::cat_tools.relation_type +$body$ + , 'cat_tools__usage' + , 'Mapping from to a ' +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__relkind' + , 'kind cat_tools.relation_type' + , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* Mapping per pg_class.h, same as relation__kind() above. */ +SELECT CASE kind + WHEN 'table' THEN 'r' + WHEN 'index' THEN 'i' + WHEN 'sequence' THEN 'S' + WHEN 'toast table' THEN 't' + WHEN 'view' THEN 'v' + WHEN 'composite type' THEN 'c' + WHEN 'foreign table' THEN 'f' + WHEN 'materialized view' THEN 'm' +END::cat_tools.relation_relkind +$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.relation__relkind' + , 'kind text' + , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.relation__relkind(kind::cat_tools.relation_type)$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind text' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$SELECT cat_tools.relation__kind(relkind::cat_tools.relation_relkind)$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +@generated@ + +CREATE OR REPLACE VIEW _cat_tools.pg_depend_identity_v AS -- SED: REQUIRES 9.3! + SELECT o.type AS object_type -- SED: REQUIRES 9.3! + , o.schema AS object_schema -- SED: REQUIRES 9.3! + , o.name AS object_name -- SED: REQUIRES 9.3! + , o.identity AS object_identity -- SED: REQUIRES 9.3! + , r.type AS reference_type -- SED: REQUIRES 9.3! + , r.schema AS reference_schema -- SED: REQUIRES 9.3! + , r.name AS reference_name -- SED: REQUIRES 9.3! + , r.identity AS reference_identity -- SED: REQUIRES 9.3! + , d.* -- SED: REQUIRES 9.3! + FROM pg_catalog.pg_depend d -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(classid, objid, objsubid) o -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(refclassid, refobjid, refobjsubid) r -- SED: REQUIRES 9.3! + WHERE classid <> 0 -- SED: REQUIRES 9.3! + UNION ALL -- SED: REQUIRES 9.3! + SELECT NULL, NULL, NULL, NULL -- SED: REQUIRES 9.3! + , r.type AS reference_type -- SED: REQUIRES 9.3! + , r.schema AS reference_schema -- SED: REQUIRES 9.3! + , r.name AS reference_name -- SED: REQUIRES 9.3! + , r.identity AS reference_identity -- SED: REQUIRES 9.3! + , d.* -- SED: REQUIRES 9.3! + FROM pg_catalog.pg_depend d -- SED: REQUIRES 9.3! + , pg_catalog.pg_identify_object(refclassid, refobjid, refobjsubid) r -- SED: REQUIRES 9.3! + WHERE classid = 0 -- SED: REQUIRES 9.3! +; -- SED: REQUIRES 9.3! + +@generated@ + +CREATE OR REPLACE VIEW cat_tools.pg_class_v AS + SELECT * + FROM _cat_tools.pg_class_v + + /* + * Oddly, there's no security associated with schema or table visibility. + * Be a bit paranoid though. + */ + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND relkind IN( 'r', 'v', 'f' ) +; +GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; + +@generated@ + +/* + * On PG11+, pg_attribute gained attmissingval (pseudo-type anyarray, not usable in views). + * Include it cast to text[] on PG11+; expose as NULL::text[] on older versions. + */ +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.pg_attribute_v AS + SELECT %s + , c.* + , t.oid AS typoid + , %s + , a.attmissingval::text::text[] AS attmissingval -- SED: REQUIRES 11! + , NULL::text[] AS attmissingval -- SED: PRIOR TO 11! + FROM pg_attribute a + LEFT JOIN _cat_tools.pg_class_v c ON ( c.reloid = a.attrelid ) + LEFT JOIN pg_type t ON ( t.oid = a.atttypid ) +; +$fmt$ + /* + * attmissingval is explicitly included above (cast to text[] via SED markers). + * Omit it here so it doesn't appear twice, and omit oid to avoid conflicts on PG12+. + */ + , __cat_tools.omit_column('pg_catalog.pg_attribute', array['oid', 'attmissingval', 'attcacheoff']) + , __cat_tools.omit_column('pg_catalog.pg_type') +)); +REVOKE ALL ON _cat_tools.pg_attribute_v FROM public; + +/* + * Computed columns come first so that future CREATE OR REPLACE VIEW upgrades + * can extend the pg_attribute_v column list (via %s) at the end without + * breaking existing column positions. TODO: once issue 13 is resolved, + * switch back to logical ordering (raw pg_attribute_v columns first, computed + * columns after). + */ +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW _cat_tools.column AS + SELECT pg_catalog.format_type(typoid, atttypmod) AS column_type + , CASE typtype + -- domain + WHEN 'd' THEN pg_catalog.format_type(typbasetype, typtypmod) + -- enum + WHEN 'e' THEN 'text' + ELSE pg_catalog.format_type(typoid, atttypmod) + END AS base_type + , pk.conkey AS pk_columns + , ARRAY[attnum] <@ pk.conkey AS is_pk_member + , (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) + FROM pg_catalog.pg_attrdef d + WHERE d.adrelid = a.attrelid + AND d.adnum = a.attnum + AND a.atthasdef + ) AS column_default + , %s + FROM _cat_tools.pg_attribute_v a + LEFT JOIN pg_constraint pk + ON ( reloid = pk.conrelid ) + AND pk.contype = 'p' +; +$fmt$ + , __cat_tools.omit_column('_cat_tools.pg_attribute_v') +)); +REVOKE ALL ON _cat_tools.column FROM public; + +@generated@ + +/* + * Starting in PG12, oid became a visible column in system catalogs. + * Use omit_column to avoid duplicate oid columns. + */ +SELECT __cat_tools.exec(format($fmt$ +CREATE OR REPLACE VIEW cat_tools.pg_extension_v AS + SELECT e.oid + , %s + + , extnamespace::regnamespace AS extschema -- SED: REQUIRES 9.5! + , nspname AS extschema -- SED: PRIOR TO 9.5! + + , extconfig::pg_catalog.regclass[] AS ext_config_tables + FROM pg_catalog.pg_extension e + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace +; +$fmt$ + , __cat_tools.omit_column('pg_catalog.pg_extension') +)); +GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage; + +CREATE OR REPLACE VIEW cat_tools.column AS + SELECT * + FROM _cat_tools.column + -- SECURITY + WHERE NOT pg_is_other_temp_schema(relnamespace) + AND attnum > 0 + AND NOT attisdropped + AND relkind IN( 'r', 'v', 'f' ) + AND ( + pg_has_role(SESSION_USER, relowner, 'USAGE'::text) + OR has_column_privilege(SESSION_USER, reloid, attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text) + ) + ORDER BY relschema, relname, attnum +; +GRANT SELECT ON cat_tools.column TO cat_tools__usage; + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +SELECT __cat_tools.create_function( + '_cat_tools._pg_sv_column_array' + , 'OID, SMALLINT[]' + , 'NAME[] LANGUAGE sql STABLE' + , $$ + SELECT ARRAY( + SELECT a.attname + FROM pg_catalog.pg_attribute a + JOIN generate_series(1, array_upper($2, 1)) s(i) ON a.attnum = $2[i] + WHERE attrelid = $1 + ORDER BY i + ) +$$ +); + +@generated@ + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +SELECT __cat_tools.create_function( + '_cat_tools._pg_sv_table_accessible' + , 'OID, OID' + , 'boolean LANGUAGE sql STABLE' + , $$ + SELECT CASE WHEN has_schema_privilege($1, 'USAGE') THEN ( + has_table_privilege($2, 'SELECT') + OR has_table_privilege($2, 'INSERT') + or has_table_privilege($2, 'UPDATE') + OR has_table_privilege($2, 'DELETE') + OR has_table_privilege($2, 'RULE') + OR has_table_privilege($2, 'REFERENCES') + OR has_table_privilege($2, 'TRIGGER') + ) ELSE FALSE + END; +$$ +); + +@generated@ + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +CREATE OR REPLACE VIEW cat_tools.pg_all_foreign_keys +AS + SELECT n1.nspname AS fk_schema_name, + c1.relname AS fk_table_name, + k1.conname AS fk_constraint_name, + c1.oid AS fk_table_oid, + _cat_tools._pg_sv_column_array(k1.conrelid,k1.conkey) AS fk_columns, + n2.nspname AS pk_schema_name, + c2.relname AS pk_table_name, + k2.conname AS pk_constraint_name, + c2.oid AS pk_table_oid, + ci.relname AS pk_index_name, + _cat_tools._pg_sv_column_array(k1.confrelid,k1.confkey) AS pk_columns, + CASE k1.confmatchtype WHEN 'f' THEN 'FULL' + WHEN 'p' THEN 'PARTIAL' + WHEN 'u' THEN 'NONE' + else null + END AS match_type, + CASE k1.confdeltype WHEN 'a' THEN 'NO ACTION' -- @generated@ + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + else null + END AS on_delete, + CASE k1.confupdtype WHEN 'a' THEN 'NO ACTION' + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + ELSE NULL + END AS on_update, + k1.condeferrable AS is_deferrable, -- @generated@ + k1.condeferred AS is_deferred + FROM pg_catalog.pg_constraint k1 + JOIN pg_catalog.pg_namespace n1 ON (n1.oid = k1.connamespace) + JOIN pg_catalog.pg_class c1 ON (c1.oid = k1.conrelid) + JOIN pg_catalog.pg_class c2 ON (c2.oid = k1.confrelid) + JOIN pg_catalog.pg_namespace n2 ON (n2.oid = c2.relnamespace) + JOIN pg_catalog.pg_depend d ON ( + d.classid = 'pg_constraint'::pg_catalog.regclass -- @generated@ + AND d.objid = k1.oid + AND d.objsubid = 0 + AND d.deptype = 'n' + AND d.refclassid = 'pg_class'::pg_catalog.regclass + AND d.refobjsubid=0 + ) + JOIN pg_catalog.pg_class ci ON (ci.oid = d.refobjid AND ci.relkind = 'i') + LEFT JOIN pg_depend d2 ON ( + d2.classid = 'pg_class'::pg_catalog.regclass -- @generated@ + AND d2.objid = ci.oid + AND d2.objsubid = 0 + AND d2.deptype = 'i' + AND d2.refclassid = 'pg_constraint'::pg_catalog.regclass + AND d2.refobjsubid = 0 + ) + LEFT JOIN pg_catalog.pg_constraint k2 ON ( -- @generated@ + k2.oid = d2.refobjid + AND k2.contype IN ('p', 'u') + ) + WHERE k1.conrelid != 0 + AND k1.confrelid != 0 + AND k1.contype = 'f' + AND _cat_tools._pg_sv_table_accessible(n1.oid, c1.oid) +; +GRANT SELECT ON cat_tools.pg_all_foreign_keys TO cat_tools__usage; + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.pg_attribute__get' + , $$ + relation pg_catalog.regclass + , column_name name +$$ + , $$pg_catalog.pg_attribute LANGUAGE plpgsql$$ + , $body$ +DECLARE + r pg_catalog.pg_attribute; +BEGIN + SELECT INTO STRICT r + * + FROM pg_catalog.pg_attribute + WHERE attrelid = relation + AND attname = column_name + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'column "%" of relation "%" does not exist', column_name, relation + USING ERRCODE = 'undefined_column' + ; +END +$body$ + , 'cat_tools__usage' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.pg_extension__get' + , 'extension_name name' + , $$cat_tools.pg_extension_v LANGUAGE plpgsql$$ + , $body$ +DECLARE + r cat_tools.pg_extension_v; +BEGIN + SELECT INTO STRICT r + * + FROM cat_tools.pg_extension_v + WHERE extname = extension_name + ; + RETURN r; +EXCEPTION WHEN no_data_found THEN + RAISE 'extension "%" does not exist', extension_name + USING ERRCODE = 'undefined_object' + ; +END +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas' + , 'extension_names name[]' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! + , $$pg_catalog.name[] LANGUAGE sql$$ -- SED: PRIOR TO 9.5! + , $body$ +SELECT array( + SELECT (cat_tools.pg_extension__get(en)).extschema + FROM unnest(extension_names) en +) +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas_unique' + , 'extension_names name[]' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! + , $$pg_catalog.name[] LANGUAGE sql$$ -- SED: PRIOR TO 9.5! + , $body$ +SELECT array( + SELECT DISTINCT (cat_tools.pg_extension__get(en)).extschema + FROM unnest(extension_names) en +) +$body$ + , 'cat_tools__usage' +); + +@generated@ + +-- Text versions +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas' + , 'extension_names text' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! + , $$pg_catalog.name[] LANGUAGE sql$$ -- SED: PRIOR TO 9.5! + , $body$ +SELECT cat_tools.extension__schemas( + CASE WHEN extension_names LIKE '{%}' THEN extension_names + ELSE '{' || extension_names || '}' + END::name[] +) +$body$ + , 'cat_tools__usage' +); +SELECT __cat_tools.create_function( + 'cat_tools.extension__schemas_unique' + , 'extension_names text' + , $$pg_catalog.regnamespace[] LANGUAGE sql$$ -- SED: REQUIRES 9.5! + , $$pg_catalog.name[] LANGUAGE sql$$ -- SED: PRIOR TO 9.5! + , $body$ +SELECT cat_tools.extension__schemas_unique( + CASE WHEN extension_names LIKE '{%}' THEN extension_names + ELSE '{' || extension_names || '}' + END::name[] +) +$body$ + , 'cat_tools__usage' +); + + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.get_serial_sequence' + , $$ + table_name text + , column_name text +$$ + , $$pg_catalog.regclass LANGUAGE plpgsql$$ + , $body$ +DECLARE + seq pg_catalog.regclass; +BEGIN + -- Note: the function will throw an error if table or column doesn't exist + seq := pg_get_serial_sequence( table_name, column_name ); + + IF seq IS NULL THEN + RAISE EXCEPTION '"%" is not a serial column', column_name + USING ERRCODE = 'wrong_object_type' + -- TODO: SCHEMA and COLUMN + , COLUMN = column_name -- SED: REQUIRES 9.3! + ; + END IF; + + RETURN seq; +END +$body$ + , 'cat_tools__usage' + , 'Return sequence that is associated with a column. Unlike the pg_get_serial_sequence, throw an exception if there is no sequence associated with the column.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.sequence__last' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.currval(cat_tools.get_serial_sequence($1,$2))' + , 'cat_tools__usage' + , 'Return the last value assigned to a column with an associated sequence.' +); +SELECT __cat_tools.create_function( + 'cat_tools.currval' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.sequence__last($1,$2)' + , 'cat_tools__usage' + , 'Return the last value assigned to a column with an associated sequence.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.sequence__next' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.nextval(cat_tools.get_serial_sequence($1,$2))' + , 'cat_tools__usage' + , 'Return the next value to assign to a column with an associated sequence. THIS ADVANCES THE SEQUENCE.' +); +SELECT __cat_tools.create_function( + 'cat_tools.nextval' + , $$ + table_name text + , column_name text +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.sequence__next($1,$2)' + , 'cat_tools__usage' + , 'Return the next value to assign to a column with an associated sequence. THIS ADVANCES THE SEQUENCE.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.setval' + , $$ + table_name text + , column_name text + , new_value bigint + , has_been_used boolean DEFAULT true +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT pg_catalog.setval(cat_tools.get_serial_sequence($1,$2), $3, $4)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. If has_been_used is true, the sequence will be set to new_value + 1. Returns new_value. See also sequence__set_last() and sequence__set_next().' +); +SELECT __cat_tools.create_function( + 'cat_tools.sequence__set_last' + , $$ + table_name text + , column_name text + , last_value bigint +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.setval($1,$2,$3,true)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. last_value is the last value used, so sequence is set to last_value+1. See also sequence__set_next().' +); +SELECT __cat_tools.create_function( + 'cat_tools.sequence__set_next' + , $$ + table_name text + , column_name text + , next_value bigint +$$ + , $$bigint LANGUAGE sql$$ + , 'SELECT cat_tools.setval($1,$2,$3,false)' + , 'cat_tools__usage' + , 'Changes the value for a sequence associated with a column. next_value is the next value the sequence will assign. See also sequence__last_value.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.enum_range' + , 'enum pg_catalog.regtype' + , $$text[] LANGUAGE plpgsql STABLE$$ + , $body$ +DECLARE + ret text[]; +BEGIN + EXECUTE format('SELECT pg_catalog.enum_range( NULL::%s )', enum) INTO ret; + RETURN ret; +END +$body$ + , 'cat_tools__usage' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.enum_range_srf' + , 'enum pg_catalog.regtype' + , $$SETOF text LANGUAGE sql$$ + , $body$ +SELECT * FROM unnest( cat_tools.enum_range($1) ) AS r(enum_label) +$body$ + , 'cat_tools__usage' +); + +SELECT __cat_tools.create_function( + 'cat_tools.pg_class' + , 'rel pg_catalog.regclass' + , $$cat_tools.pg_class_v LANGUAGE sql STABLE$$ + , $body$ +SELECT * FROM cat_tools.pg_class_v WHERE reloid = $1 +$body$ + , 'cat_tools__usage' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.name__check' + , 'name_to_check text' + , $$void LANGUAGE plpgsql$$ + , $body$ +BEGIN + IF name_to_check IS DISTINCT FROM name_to_check::name THEN + RAISE '"%" becomes "%" when cast to name', name_to_check, name_to_check::name; + END IF; +END +$body$ + , 'cat_tools__usage' +); + +@generated@ + +/* + * Trigger functions + */ +SELECT __cat_tools.create_function( + 'cat_tools.trigger__get_oid__loose' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text +$$ + , $$oid LANGUAGE sql$$ + , $body$ + SELECT oid + FROM pg_trigger + WHERE tgrelid = $1 --trigger_table + AND tgname = CASE + /* + * tgname isn't quoted, so strip quotes, but only if the string both + * starts and ends with quotes + */ + WHEN $2 LIKE '"%"' THEN btrim($2, '"') + ELSE $2 + END --trigger_name + ; +$body$ + , 'cat_tools__usage' + , 'Return the OID for a trigger. Returns NULL if trigger does not exist.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__get_oid' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text +$$ + , $$oid LANGUAGE plpgsql$$ + , $body$ +DECLARE + v_oid oid; +BEGIN + -- Note that because __loose isn't an SRF it'll always return a value + v_oid := cat_tools.trigger__get_oid__loose( trigger_table, trigger_name ) ; + + IF v_oid IS NULL THEN + RAISE EXCEPTION 'trigger % on table % does not exist', trigger_name, trigger_table + USING errcode = 'undefined_object' -- 42704 + ; + END IF; + + RETURN v_oid; +END +$body$ + , 'cat_tools__usage' + , 'Return the OID for a trigger. Throws an undefined_object error if the trigger does not exist.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__parse' + , $$ + trigger_oid oid + , OUT trigger_table regclass + , OUT timing text + , OUT events text[] + , OUT defer text + , OUT row_statement text + , OUT when_clause text + , OUT trigger_function regprocedure + , OUT function_arguments text[] +$$ + , $$record STABLE LANGUAGE plpgsql$$ + , $body$ +DECLARE + r_trigger pg_catalog.pg_trigger; + v_triggerdef text; + v_create_stanza text; + v_on_clause text; + v_execute_clause text; + + v_work text; + v_array text[]; +BEGIN + /* + * Do this first to make sure trigger exists. + * + * TODO: After we no longer support < 9.6, test v_triggerdef for NULL instead + * using the extra block here. + */ + BEGIN + SELECT * INTO STRICT r_trigger FROM pg_catalog.pg_trigger WHERE oid = trigger_oid; + EXCEPTION WHEN no_data_found THEN + RAISE EXCEPTION 'trigger with OID % does not exist', trigger_oid + USING errcode = 'undefined_object' -- 42704 + ; + END; + trigger_table := r_trigger.tgrelid; + trigger_function := r_trigger.tgfoid; + + v_triggerdef := pg_catalog.pg_get_triggerdef(trigger_oid, true); + + v_create_stanza := format( + 'CREATE %sTRIGGER %I ' + , CASE WHEN r_trigger.tgconstraint=0 THEN '' ELSE 'CONSTRAINT ' END + , r_trigger.tgname + ); + -- Strip CREATE [CONSTRAINT] TRIGGER ... off + v_work := replace( v_triggerdef, v_create_stanza, '' ); + + -- Get BEFORE | AFTER | INSTEAD OF + timing := split_part( v_work, ' ', 1 ); + timing := timing || CASE timing WHEN 'INSTEAD' THEN ' OF' ELSE '' END; + + -- Strip off timing clause + v_work := replace( v_work, timing || ' ', '' ); + + -- Get array of events (INSERT, UPDATE [OF column, column], DELETE, TRUNCATE) + v_on_clause := ' ON ' || r_trigger.tgrelid::pg_catalog.regclass || ' '; + v_array := regexp_split_to_array( v_work, v_on_clause ); + events := string_to_array( v_array[1], ' OR ' ); + -- Get everything after ON table_name + v_work := v_array[2]; + RAISE DEBUG 'v_work "%"', v_work; + + -- Strip off FROM referenced_table if we have it + IF r_trigger.tgconstrrelid<>0 THEN + v_work := replace( + v_work + , 'FROM ' || r_trigger.tgconstrrelid::pg_catalog.regclass || ' ' + , '' + ); + END IF; + RAISE DEBUG 'v_work "%"', v_work; + + -- Get function arguments + -- PG11+ uses "EXECUTE FUNCTION"; older versions use "EXECUTE PROCEDURE". + -- Note: ::regproc returns the internal pg_temp_N schema name while pg_get_triggerdef + -- uses the pg_temp alias, so we match on EXECUTE PROCEDURE/FUNCTION + any non-space + -- chars (the function name) rather than the specific function name. + v_execute_clause := E' EXECUTE (?:PROCEDURE|FUNCTION) \\S+\\('; + v_array := regexp_split_to_array( v_work, v_execute_clause ); + EXECUTE CASE + WHEN trim(rtrim(v_array[2], ')')) = '' THEN 'SELECT ARRAY[]::text[]' + ELSE format('SELECT array[ %s ]', rtrim( v_array[2], ')' )) + END + INTO function_arguments + ; + RAISE DEBUG 'v_array[2] "%"', v_array[2]; + -- Get everything prior to EXECUTE PROCEDURE ... + v_work := v_array[1]; + RAISE DEBUG 'v_work "%"', v_work; + + row_statement := (regexp_matches( v_work, 'FOR EACH (ROW|STATEMENT)' ))[1]; + + -- Get [ NOT DEFERRABLE | [ DEFERRABLE ] { INITIALLY IMMEDIATE | INITIALLY DEFERRED } ] + v_array := regexp_split_to_array( v_work, 'FOR EACH (ROW|STATEMENT)' ); + RAISE DEBUG 'v_work = "%", v_array = "%"', v_work, v_array; + defer := rtrim(v_array[1]); + + IF r_trigger.tgqual IS NOT NULL THEN + when_clause := rtrim( + (regexp_split_to_array( v_array[2], E' WHEN \\(' ))[2] + , ')' + ); + END IF; + + RAISE DEBUG +$$v_create_stanza = "%" + v_on_clause = "%" + v_execute_clause = "%"$$ + , v_create_stanza + , v_on_clause + , v_execute_clause + ; + + RETURN; +END +$body$ + , 'cat_tools__usage' + , 'Provide details about a trigger.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__parse' + , $$ + trigger_table pg_catalog.regclass + , trigger_name text + , OUT timing text + , OUT events text[] + , OUT defer text + , OUT row_statement text + , OUT when_clause text + , OUT trigger_function regprocedure + , OUT function_arguments text[] +$$ + , $$record STABLE LANGUAGE sql$$ + -- s/, OUT \(\w*\).*/ , \1/ + , $body$ +SELECT + timing + , events + , "defer" + , row_statement + , when_clause + , trigger_function + , function_arguments + FROM cat_tools.trigger__parse( + cat_tools.trigger__get_oid(trigger_table, trigger_name) + ) +$body$ + , 'cat_tools__usage' + , 'Provide details about a trigger.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.trigger__args_as_text' + , $$function_arguments text[]$$ + , $$text IMMUTABLE STRICT LANGUAGE sql$$ + , $body$ + SELECT format( + $$'%s'$$ + , array_to_string( + function_arguments + , $$', '$$ + ) + ) +$body$ + , 'cat_tools__usage' + , 'Convert function_arguments as returned by trigger__parse() to text (for backwards compatibility).' +); + +@generated@ + +INSERT INTO _cat_tools.catalog_metadata(object_catalog, reg_type, namespace_field) +SELECT object__catalog + , CASE object__catalog + WHEN 'pg_catalog.pg_class'::pg_catalog.regclass THEN 'pg_catalog.regclass' + WHEN 'pg_catalog.pg_ts_config'::pg_catalog.regclass THEN 'pg_catalog.regconfig' + WHEN 'pg_catalog.pg_ts_dict'::pg_catalog.regclass THEN 'pg_catalog.regdictionary' + WHEN 'pg_catalog.pg_namespace'::pg_catalog.regclass THEN 'pg_catalog.regnamespace' -- SED: REQUIRES 9.5! + WHEN 'pg_catalog.pg_operator'::pg_catalog.regclass THEN 'pg_catalog.regoperator' + WHEN 'pg_catalog.pg_proc'::pg_catalog.regclass THEN 'pg_catalog.regprocedure' + WHEN 'pg_catalog.pg_authid'::pg_catalog.regclass THEN 'pg_catalog.regrole' -- SED: REQUIRES 9.5! + WHEN 'pg_catalog.pg_type'::pg_catalog.regclass THEN 'pg_catalog.regtype' + END::pg_catalog.regtype + , n.attname + FROM ( + SELECT DISTINCT cat_tools.object__catalog(object_type) + FROM cat_tools.enum_range_srf('cat_tools.object_type') r(object_type) + ) d + LEFT JOIN cat_tools.column n + ON n.attrelid = object__catalog + AND n.attname ~ 'namespace$' + AND atttypid = 'oid'::pg_catalog.regtype +; +UPDATE _cat_tools.catalog_metadata + SET simple_reg_type = 'pg_catalog.regproc' + WHERE object_catalog = 'pg_catalog.pg_proc'::pg_catalog.regclass +; +UPDATE _cat_tools.catalog_metadata + SET simple_reg_type = 'pg_catalog.regoper' + WHERE object_catalog = 'pg_catalog.pg_operator'::pg_catalog.regclass +; +-- Cluster to get rid of dead rows +CLUSTER _cat_tools.catalog_metadata USING catalog_metadata__pk_object_catalog; + +@generated@ + +/* + * Drop "temporary" objects + */ +DROP FUNCTION __cat_tools.omit_column( + rel text + , omit name[] +); +DROP FUNCTION __cat_tools.exec( + sql text +); +DROP FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text + , comment text +); +DROP SCHEMA __cat_tools; + +-- vi: expandtab ts=2 sw=2 diff --git a/test/build/expected/upgrade.out b/test/build/expected/upgrade.out new file mode 100644 index 0000000..25fdbb1 --- /dev/null +++ b/test/build/expected/upgrade.out @@ -0,0 +1 @@ +\set ECHO none diff --git a/test/build/upgrade.sql b/test/build/upgrade.sql new file mode 100644 index 0000000..11a464b --- /dev/null +++ b/test/build/upgrade.sql @@ -0,0 +1,39 @@ +\set ECHO none +\i test/pgxntool/psql.sql +\t + +/* + * Regression guard for the CONDITIONAL 0.2.2->0.2.3 catalog-view rebuild. + * + * A fresh 0.2.2 install has CORRECT catalog views (built with the fixed + * `!= ALL` omit_column), so the 0.2.2->0.2.3 update must leave the public view + * chain -- cat_tools.pg_class_v, cat_tools.column, and the cat_tools.pg_class() + * function -- and anything depending on it, in place. The old UNCONDITIONAL + * DROP+CREATE would abort here: it drops those objects with no CASCADE, so the + * dependents below would make ALTER EXTENSION UPDATE error out (failing this + * build test). The conditional rebuild skips a correct install, so the + * dependents survive. + */ +BEGIN; +CREATE EXTENSION cat_tools VERSION '0.2.2'; + +CREATE VIEW pg_temp.dep_on_pg_class_v AS SELECT * FROM cat_tools.pg_class_v; +CREATE VIEW pg_temp.dep_on_column AS SELECT * FROM cat_tools.column; + +-- Suppress any expected notices from the update so build.out stays empty. +SET LOCAL client_min_messages = ERROR; +ALTER EXTENSION cat_tools UPDATE; + +/* + * Prove the dependents still exist and are queryable after the update; a bare + * DO block emits no output, so the expected build output stays empty. + */ +DO $$ +BEGIN + PERFORM 1 FROM pg_temp.dep_on_pg_class_v; + PERFORM 1 FROM pg_temp.dep_on_column; +END +$$; +ROLLBACK; + +-- vi: expandtab sw=2 ts=2