diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e245f9f..65a452e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: test: strategy: matrix: - pg: [18, 17, 16, 15, 14, 13, 12, 11, 10] + pg: [18, 17, 16, 15, 14, 13, 12] name: 🐘 PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -25,27 +25,60 @@ jobs: run: pg-start ${{ matrix.pg }} - name: Check out the repo uses: actions/checkout@v6 - - name: Install rsync - run: apt-get install -y rsync + - name: Install rsync and server headers + # postgresql-server-dev-NN provides catalog/pg_class.h, which + # test/gen-relkinds.sh reads for the relkind drift check in + # test/sql/relation__.sql. + run: apt-get install -y rsync postgresql-server-dev-${{ matrix.pg }} - name: Test on PostgreSQL ${{ matrix.pg }} - run: make test + run: | + # Fail if the relkind drift source is empty (headers missing): the + # drift check must actually run on every version, not pass silently. + make check-relkind-source + make test + # `make test` never exits non-zero on regressions (pgxntool marks + # installcheck `.IGNORE`), so gate the build explicitly on the + # pgtap/regression.diffs check or failures would pass silently. + make verify-results + # Proves cat_tools survives a BINARY pg_upgrade (in-place catalog migration to a + # newer PostgreSQL major), not just an in-place extension update. Installs an old + # cat_tools on an old cluster, plants a dependency guard, binary-pg_upgrades to a + # newer cluster, updates the extension to current, then runs the suite against + # the REAL migrated objects in "existing" mode. Catches view/catalog breakage + # that only surfaces when objects created on an old server are read on a new one. pg-upgrade-test: strategy: matrix: include: + # old_pg=10: the FULL real-world journey for a user on old PostgreSQL + + # old cat_tools. 0.2.0 installs on PG10; the BRIDGE update to 0.2.3 + # (run on PG10, before pg_upgrade) is what makes pg_upgrade safe. The + # shipped 0.2.0β†’0.2.2 / 0.2.1β†’0.2.2 scripts do NOT fix the views (their + # omit_column used the no-op `!= ANY`, so relhasoids/relhaspkey survive + # in _cat_tools.pg_class_v); the pg_class_v DROP+CREATE rebuild that + # strips those columns lives in the 0.2.2β†’0.2.3 update. So we must + # bridge THROUGH 0.2.3 before pg_upgrade. 0.2.3 is also the furthest a + # PG10 cluster can reach: 0.2.3β†’0.3.0 uses ALTER TYPE ... ADD VALUE, + # which cannot run in an extension update script before PG12. After + # pg_upgrade to the new cluster, the post-upgrade step updates 0.2.3β†’ + # current (0.3.0). This proves a 0.2.2/0.2.3 reached via the bridge + # update (not a fresh install) survives pg_upgrade and updates to latest. - old_pg: "10" - new_pg: "11" - - old_pg: "10" - new_pg: "18" - - old_pg: "11" - new_pg: "12" - - old_pg: "11" new_pg: "18" + old_install: "0.2.0" + bridge_to: "0.2.3" + # old_pg>=11: 0.2.0/0.2.1 do not install on PG11+, and such users are + # already on 0.2.2+. Install 0.2.2 directly (no bridge), pg_upgrade, + # then update to current. - old_pg: "12" new_pg: "13" + old_install: "0.2.2" + bridge_to: "" - old_pg: "12" new_pg: "18" + old_install: "0.2.2" + bridge_to: "" name: πŸ”„ Binary pg_upgrade ${{ matrix.old_pg }} β†’ ${{ matrix.new_pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -72,8 +105,17 @@ jobs: run: apt-get install -y rsync - name: Install cat_tools into old cluster run: make install - - name: Install cat_tools extension into upgrade test database - run: psql -c "CREATE EXTENSION cat_tools" + - name: Prepare the old cluster (install + bridge + dependency guard) + # prepare-old installs cat_tools at old_install and, when bridge_to is set + # (old_pg=10), ALTER EXTENSION UPDATEs to it BEFORE pg_upgrade -- the + # bridge that makes the views pg_upgrade-safe (see prepare-old in the + # script). It then plants + proves the dependency guard so the later + # existing-mode run cannot silently drop+reinstall and test a fresh + # install instead. The extension is always older than current, so the + # post-upgrade ALTER EXTENSION UPDATE has real work to do. + run: >- + bin/test_existing prepare-old cat_tools_upgrade + "${{ matrix.old_install }}" "${{ matrix.bridge_to }}" - name: Install PostgreSQL ${{ matrix.new_pg }} run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} - name: Install cat_tools into new cluster @@ -100,36 +142,44 @@ jobs: /var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \ -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } pg_ctlcluster ${{ matrix.new_pg }} test start - - name: Verify extension version after upgrade - run: | - INSTALLED=$(psql -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") - EXPECTED=$(make -s print-PGXNVERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p') - echo "installed=$INSTALLED expected=$EXPECTED" - # Assert installed version (reported by pg_extension) matches what's specified in META.in.json - if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then - echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1 - fi - - name: Run test suite on upgraded cluster - run: make test - # TODO: also test ALTER EXTENSION cat_tools UPDATE here, once the - # pg_upgrade source versions (e.g. 0.2.0) can install on the new_pg - # version. Currently the pre-0.2.2 install scripts fail on PG11+ so - # we cannot pg_upgrade from a cluster that has them installed. + - name: Update the pg_upgraded extension to the current version + # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects (the + # extension binary pg_upgrade just migrated), running the + # version-to-version scripts up to the current version. The 0.2.3 -> 0.3.0 + # script uses ALTER TYPE ... ADD VALUE, which is why new_pg must be >= 12. + run: bin/test_existing update cat_tools_upgrade + - name: Run the suite against the pg_upgraded database (existing mode) + # run-suite asserts the version, re-proves the dependency guard still + # blocks a non-CASCADE drop (i.e. it survived pg_upgrade), then runs the + # suite against the REAL pg_upgraded + updated database via --use-existing + # (so pg_regress does not drop/recreate it) -- a plain fresh `make test` + # would silently test a fresh install instead of the migrated objects. + run: bin/test_existing run-suite cat_tools_upgrade + # Proves the in-place extension update path: CREATE EXTENSION at an OLD cat_tools + # version then ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL). On PG12+ + # it updates 0.2.2 -> current and runs the FULL suite against the updated + # database (same expected output as a fresh install, so an updated DB must behave + # identically). The PG10 leg only exercises the pre-0.2.2 update scripts, the + # sole version where they still load. Complements pg-upgrade-test, which covers + # the cross-major-version binary upgrade instead. extension-update-test: strategy: matrix: - # Restricted to PG10 only because the pre-0.2.2 install scripts use - # unqualified SELECT * in views over pg_attribute/pg_constraint: - # - PG11 added attmissingval (pseudo-type anyarray) to pg_attribute, - # so cat_tools--0.2.0.sql and cat_tools--0.2.1.sql fail on PG11+ - # with "column attmissingval has pseudo-type anyarray". - # - PG12 made the `oid` system column visible in SELECT *, causing - # "column oid specified more than once" on PG12+. - # PG10 is the only version where both old scripts install cleanly. - # TODO: when a future version's install script works on PG11+, expand - # this matrix and add tests for the update path from 0.2.2 onwards. - pg: [10] + # PG12+: exercise the WIDEST update path we support β€” CREATE EXTENSION at + # the 0.2.2 backward-compat floor, ALTER EXTENSION UPDATE to the CURRENT + # version, and run the full suite against the updated database. 0.2.2 is + # the floor because the 0.2.0/0.2.1 install scripts fail on PG11+/PG12+; + # PG12 is the PostgreSQL floor because the update runs + # `ALTER TYPE ... ADD VALUE`, which PG11 and below cannot run in an + # extension update script (lifted in PG12). + # PG10: the ONLY version where the pre-0.2.2 install scripts still load, + # so the only place the 0.2.0->0.2.2 and 0.2.1->0.2.2 update scripts can + # be exercised. They target 0.2.2 (not the current version) and use no + # ALTER TYPE ... ADD VALUE, so they run on PG10. The PG10 leg runs only + # those two checks β€” not the current-version suite (0.3.0 needs PG12+). + # See the per-step `if` guards. + pg: [18, 17, 16, 15, 14, 13, 12, 10] name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -138,36 +188,36 @@ jobs: run: pg-start ${{ matrix.pg }} - name: Check out the repo uses: actions/checkout@v6 + - name: Install rsync and server headers + # server-dev provides catalog/pg_class.h for the relkind drift check + # (see the "test" job); required so check-relkind-source below passes. + # PG10 runs only the legacy-script checks (no suite), so it needs no headers. + if: matrix.pg != '10' + run: apt-get install -y rsync postgresql-server-dev-${{ matrix.pg }} - name: Install rsync + if: matrix.pg == '10' run: apt-get install -y rsync - name: Install cat_tools (all versions) run: make install - - name: Test update from 0.2.0 to current + - name: Test pre-0.2.2 update scripts (0.2.0/0.2.1 β†’ 0.2.2, PG10 only) + # 0.2.0/0.2.1 install only on PG10; their update scripts target 0.2.2 (not + # the current version) and are otherwise never exercised. PG takes the + # shortest path, so 0.2.0 β†’ 0.2.2 uses the direct 0.2.0--0.2.2 script; + # starting at 0.2.1 exercises the 0.2.1--0.2.2 script directly. update-check + # asserts each lands on 0.2.2. No suite runs (0.3.0 needs PG12+). + if: matrix.pg == '10' run: | - psql -c "CREATE EXTENSION cat_tools VERSION '0.2.0'" - psql -c "ALTER EXTENSION cat_tools UPDATE" - INSTALLED=$(psql -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") - EXPECTED=$(make -s print-PGXNVERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p') - echo "installed=$INSTALLED expected=$EXPECTED" - # 0.2.0 -> current; the 0.2.1->0.2.2 script is covered by the next step. - if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then - echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1 - fi - - 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 - # direct 0.2.0->0.2.2 and never runs the 0.2.1->0.2.2 script. Start at 0.2.1 - # and target 0.2.2 explicitly so that script is exercised. - createdb cat_tools_from_021 - psql -d cat_tools_from_021 -c "CREATE EXTENSION cat_tools VERSION '0.2.1'" - psql -d cat_tools_from_021 -c "ALTER EXTENSION cat_tools UPDATE TO '0.2.2'" - INSTALLED=$(psql -d cat_tools_from_021 -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") - echo "installed=$INSTALLED" - if [ -z "$INSTALLED" ] || [ "$INSTALLED" != "0.2.2" ]; then - echo "FAIL: expected 0.2.2, got '$INSTALLED'"; exit 1 - fi - - name: Run test suite on updated extension - run: make test + bin/test_existing update-check cat_tools_from_020 0.2.0 0.2.2 + bin/test_existing update-check cat_tools_from_021 0.2.1 0.2.2 + - name: Update 0.2.2 β†’ current and run the suite (existing mode, PG12+) + # update-scenario creates a real database at 0.2.2, plants + proves the + # dependency guard, ALTER EXTENSION UPDATEs to the current version, and + # runs the suite against that updated database in existing mode (asserting + # the version and that the guard still blocks a drop). Reusing the SAME + # suite and expected output asserts the updated database behaves + # identically to a fresh install. + if: matrix.pg != '10' + run: bin/test_existing update-scenario cat_tools_update 0.2.2 # A single stable check name for use as a required status check in branch # protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14" diff --git a/.gitignore b/.gitignore index 6fc6141..8dfbc48 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,6 @@ control.mk # built targets # Note: Version-specific files (sql/*--*.sql) are now tracked in git and should be committed -# test targets -/test/.build/ - # Test artifacts results/ regression.diffs diff --git a/CLAUDE.md b/CLAUDE.md index 05e77e2..70283c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,31 +37,88 @@ 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. +## References to PRs and issues in committed files + +Any reference to a GitHub PR or issue inside a **committed file** (SQL/code comments, +`.github/workflows/ci.yml` comments, `CLAUDE.md`, `test/install/load.sql`, docs) MUST be a +full URL, e.g. `https://github.com/Postgres-Extensions/cat_tools/issues/28` β€” never a bare +`#28` (a bare number is meaningless when the file is read outside GitHub). Referencing by +number is fine only in GitHub-native text (PR/issue titles and descriptions, review +comments). + ## SQL file conventions 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 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. -## CI: extension-update-test matrix - -The `extension-update-test` job in `.github/workflows/ci.yml` is currently restricted to -`pg: [10]` because that is the only PostgreSQL version where the pre-0.2.2 install scripts -install cleanly: -- PG 11 added `attmissingval` (pseudo-type `anyarray`) to `pg_attribute`; the old `SELECT *` - in `0.2.0`/`0.2.1` tries to include it directly, failing with "column attmissingval has - pseudo-type anyarray". -- PG 12+ exposed the `oid` system column in `SELECT *`, breaking `0.2.0`/`0.2.1` with - "column oid specified more than once". - -**When working on a new version:** review and expand this matrix. The new version's install +## CI: PostgreSQL version support + +**Policy:** Never support a fresh install on any PostgreSQL version where the extension +update path is known to be broken β€” a version that cannot be updated to is not truly +supported. + +Both PG10 and PG11 are dropped as of 0.3.0. The `ALTER TYPE ... ADD VALUE` statements in +the update script cannot run inside an extension update script on PG11 or earlier +(PROCESS_UTILITY_QUERY context); this restriction was lifted in PG12. Because a version +that cannot be updated to is not truly supported, PG10 and PG11 support is dropped +entirely. cat_tools 0.3.0 supports PG12+. + +### Test-load modes (`TEST_LOAD_SOURCE`) + +`test/install/load.sql` installs the suite's dependencies once, committed, before the pgTAP +suite. The Makefile exports the mode (and update range) as placeholder GUCs via `PGOPTIONS`; +`TEST_LOAD_SOURCE` must be `fresh`, `update`, or `existing` (anything else is a hard +parse-time error): + +- **fresh** (default): `CREATE EXTENSION cat_tools` at the current version. +- **update**: `CREATE EXTENSION` at `TEST_UPDATE_FROM` (default `0.2.2`) then + `ALTER EXTENSION cat_tools UPDATE` β€” to `TEST_UPDATE_TO` if set, else to the current + version. Running the SAME suite/expected output asserts an updated database behaves + identically to a fresh install. `make test-update` is the shorthand. +- **existing**: the extension is ALREADY installed (by binary `pg_upgrade`, or an + `ALTER EXTENSION UPDATE` performed outside the suite). load.sql does not touch it; it only + asserts presence + current version and creates the test roles. Pair with + `CONTRIB_TESTDB= EXTRA_REGRESS_OPTS=--use-existing` so `pg_regress` runs against that + database instead of dropping/recreating a throwaway one. + +### CI jobs + +- `extension-update-test` exercises the widest update path we support β€” `0.2.2` β†’ current + in `update` mode β€” on `pg: [12..18]`, plus a PG10-only leg that exercises the pre-0.2.2 + update scripts (`0.2.0`β†’`0.2.2` and `0.2.1`β†’`0.2.2`, which install only on PG10 and target + `0.2.2`, not the current version). `0.2.2` is the update-from floor only for backward-compat: + the 0.2.0/0.2.1 install scripts fail on PG11+/PG12+. PG12 is the PostgreSQL floor β€” PG11 and + earlier cannot run `ALTER TYPE ... ADD VALUE` in extension update scripts (lifted in PG12). +- `pg-upgrade-test` binary-`pg_upgrade`s a real database and then runs the suite against it + in `existing` mode. Two shapes: + - `old_pg>=11`: install `0.2.2` directly β†’ plant guard β†’ pg_upgrade β†’ update to current. + A *fresh* `0.2.2` install builds the views with the pg_upgrade-safe `omit_column` (`!= ALL`), + so it survives pg_upgrade as-is. + - `old_pg=10`: the FULL real-world journey β€” install `0.2.0`, **bridge-update to `0.2.3` on + the old cluster** β†’ plant guard β†’ pg_upgrade β†’ update to current. The bridge must reach + `0.2.3`, not `0.2.2`: the shipped `0.2.0`β†’`0.2.2` / `0.2.1`β†’`0.2.2` scripts do NOT fix the + views (their `omit_column` used the no-op `!= ANY`, leaving `relhasoids`/`relhaspkey` in + `_cat_tools.pg_class_v`); the pg_class_v DROP+CREATE rebuild that strips those columns lives + in the `0.2.2`β†’`0.2.3` update. `0.2.3` is also the furthest a PG10 cluster can reach + (`0.2.3`β†’`0.3.0` uses `ALTER TYPE ... ADD VALUE`, unrunnable in an update script before + PG12); the post-upgrade step then updates `0.2.3`β†’current on the new cluster. This proves a + `0.2.3` reached via the bridge update (not a fresh install) survives pg_upgrade. + Both flows plant the dependency guard and run through `bin/test_existing`. + +**When working on a new version:** review and expand these matrices. A new version's install script may support more PG versions, enabling testing of the update path from older cat_tools versions on newer PostgreSQL. @@ -79,6 +136,16 @@ Always use block comment format for multi-line comments in SQL files: Never use `--` line comments for multi-line explanations. +### Closing non-indentable blocks +When closing a code block that cannot be indented to show its nesting (e.g. SQL +`\endif`, `DO $$...$$`, shell heredocs, column-0 `fi`/`esac`) AND that block +contains nested blocks, label the closer with a comment naming which block it +closes β€” e.g. shell `esac # basename dispatch`, or a named dollar-quote +`DO $DO$ ... $DO$` for a DO block. Where the language rejects a trailing comment +on the closer (psql `\endif` warns "extra argument ignored"), put the label on +the immediately following line instead (e.g. `\endif` then +`-- end \if :cat_tools_mode_existing`). + ### Terminology "upgrade" refers to a PostgreSQL cluster (`pg_upgrade`); "update" refers to an extension (`ALTER EXTENSION ... UPDATE`). cat_tools' version-to-version scripts are "update scripts" β€” diff --git a/HISTORY.asc b/HISTORY.asc index b25e350..a9858ff 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,7 +1,46 @@ +0.3.0 +----- +New functions and types for working with routines and partitioned relations. + +### PostgreSQL Version Support + +**PostgreSQL 10 and 11 are no longer supported** as of this release. The 0.2.3β†’0.3.0 +update script adds values to existing enum types (`ALTER TYPE ... ADD VALUE`), which +cannot run inside an extension update script on PG11 or earlier (a restriction lifted +in PG12). Because a version that cannot be updated to is not truly supported, PG10 and +PG11 support is dropped entirely. + +cat_tools 0.3.0 supports **PostgreSQL 12 through 18+**. + +### Changes + +* Added routine/function/procedure introspection support: new enum types + `routine_type`, `routine_argument_mode`, `routine_volatility`, and + `routine_parallel_safety`, plus the composite type `routine_argument`. +* Added routine functions for parsing and retrieving argument information + (`routine__parse_arg_types`, `routine__parse_arg_names`, `routine__arg_types`, + `routine__arg_names`, and their `_text` variants), along with the mapping + functions `routine__type`, `routine__argument_mode`, `routine__volatility`, + and `routine__parallel_safety`. +* `function__arg_types()` and `function__arg_types_text()` are now deprecated in + favor of `routine__parse_arg_types()` and `routine__parse_arg_types_text()`. +* Corrected the `relation__kind()` / `relation__relkind()` mapping for `relkind` + values `c`, `f`, and `m` (composite type, foreign table, and materialized + view respectively), which were previously mapped incorrectly. +* `_cat_tools` helper functions now `REVOKE EXECUTE ... FROM PUBLIC` so they are + no longer callable by roles without `cat_tools__usage`. + +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 -provides an upgrade path from 0.2.0 and 0.2.1. +provides an update path from 0.2.0 and 0.2.1. ### PostgreSQL Version Support @@ -15,24 +54,24 @@ It fails on newer versions due to catalog schema changes: * **PG 12**: System catalog `oid` columns became visible as regular columns. Views using `SELECT c.*` alongside an explicit `c.oid` alias produce duplicate column names. -cat_tools 0.2.2 installs and upgrades correctly on **PostgreSQL 9.2 through 18+** +cat_tools 0.2.2 installs and updates correctly on **PostgreSQL 9.2 through 18+** (all currently supported versions). -### Upgrade Path +### Update Path -**You must upgrade cat_tools to 0.2.2 before upgrading PostgreSQL to version 11 or -later.** Upgrade via `ALTER EXTENSION cat_tools UPDATE`. +**You must update cat_tools to 0.2.2 before upgrading PostgreSQL to version 11 or +later.** Update via `ALTER EXTENSION cat_tools UPDATE`. -### Upgrade Warning: Objects Depending on `cat_tools.column` +### Update Warning: Objects Depending on `cat_tools.column` `cat_tools.column` in 0.2.0 and 0.2.1 had extra columns: an unqualified `SELECT *` across a `LEFT JOIN pg_constraint` accidentally pulled in all `pg_constraint` columns -(`conname`, `contype`, etc.). The 0.2.2 upgrade fixes the column list by dropping and +(`conname`, `contype`, etc.). The 0.2.2 update fixes the column list by dropping and recreating `cat_tools.column` (and its underlying `_cat_tools.column`). **If you have created any views, functions, or other objects that depend on -`cat_tools.column`, you must drop them before upgrading and recreate them -afterward.** The upgrade will fail with an error if any such dependent objects +`cat_tools.column`, you must drop them before updating and recreate them +afterward.** The update will fail with an error if any such dependent objects exist β€” this is intentional, to avoid silently breaking user-defined objects. After dropping your dependent objects, run `ALTER EXTENSION cat_tools UPDATE` again. @@ -56,17 +95,17 @@ to PostgreSQL 12 or later.** ### Changes -* `sql/cat_tools--0.1.4--0.1.5.sql` was empty; added `-- empty upgrade` placeholder so - PostgreSQL accepts it as a valid (no-op) upgrade script. +* `sql/cat_tools--0.1.4--0.1.5.sql` was empty; added `-- empty update` placeholder so + PostgreSQL accepts it as a valid (no-op) update script. * `cat_tools.column` now exposes `attmissingval` as `text[]` (cast from `anyarray` on PG 11+, or `NULL::text[]` on older versions). Any `SELECT *` on `cat_tools.column` will now include this column. * Views rebuilt using `omit_column()` to enumerate columns explicitly, avoiding both the `anyarray` and duplicate-`oid` problems. -* A direct `0.2.0 β†’ 0.2.2` upgrade path is provided (`cat_tools--0.2.0--0.2.2.sql`), +* A direct `0.2.0 β†’ 0.2.2` update path is provided (`cat_tools--0.2.0--0.2.2.sql`), which also applies all 0.2.1 function additions in a single step. * `GRANT SELECT ON cat_tools.pg_extension_v TO cat_tools__usage` is now applied on the - upgrade path from 0.2.0 (it was absent in 0.2.0 and only added via the 0.2.1 upgrade). + update path from 0.2.0 (it was absent in 0.2.0 and only added via the 0.2.1 update). * (Re-release) `_cat_tools.pg_class_v` now explicitly omits `relhasoids` (removed in PG12) and `relhaspkey` (removed in PG17) to prevent `pg_upgrade` failures. * (Re-release) `_cat_tools.pg_attribute_v` now explicitly omits `attcacheoff` (removed in @@ -74,7 +113,7 @@ to PostgreSQL 12 or later.** 0.2.1 ----- -Fix significant problems with a previous upgrade script. +Fix significant problems with a previous update script. Add pg_extension_v and related functions. @@ -114,7 +153,7 @@ objects would cascede to the extension itself. 0.1.2 ----- -=## BUGFIX: Install upgrade script +=## BUGFIX: Install update script 0.1.1 ----- diff --git a/META.in.json b/META.in.json index 932f245..9af04e1 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.3.0", "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.3.0", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", @@ -72,7 +72,7 @@ "build": { "requires": { - "PostgreSQL": "9.2.0" + "PostgreSQL": "12.0" } }, "runtime": { diff --git a/META.json b/META.json index b1a5a17..aa20b17 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.3.0", "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.3.0", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Tools for interfacing with the catalog", @@ -72,7 +72,7 @@ "build": { "requires": { - "PostgreSQL": "9.2.0" + "PostgreSQL": "12.0" } }, "runtime": { diff --git a/Makefile b/Makefile index b7774e5..bb7e699 100644 --- a/Makefile +++ b/Makefile @@ -1,51 +1,87 @@ testdeps: $(wildcard test/*.sql test/helpers/*.sql) # Be careful not to include directories in this -# Versioned SQL is generated from .sql.in at build time. That generation and the -# DATA list that installs it live in sql.mk, which also owns -# `include pgxntool/base.mk` (base.mk has no include guard, so it must be -# included exactly once; sql.mk includes it at its top). sql.mk documents the -# GNU Make two-phase (parse vs. recipe) hazards involved (e.g. -# https://github.com/Postgres-Extensions/cat_tools/issues/28) and relies on -# base.mk/control.mk/PGXS vars (EXTENSION_VERSION_FILES, PG_CONFIG, MAJORVER, -# datadir, ...). -include sql.mk - -# Support for upgrade test +# Committed-once install of the extension + test roles. # -# TODO: Instead of all of this stuff figure out how to pass something to -# pg_regress that will alter the behavior of the test instead. -TEST_BUILD_DIR = test/.build -testdeps: $(TEST_BUILD_DIR)/dep.mk $(TEST_BUILD_DIR)/active.sql --include $(TEST_BUILD_DIR)/dep.mk - -# Ensure dep.mk exists. -$(TEST_BUILD_DIR)/dep.mk: $(TEST_BUILD_DIR) - echo 'TEST_LOAD_SOURCE = new' > $(TEST_BUILD_DIR)/dep.mk - -.PHONY: set-test-new -set-test-new: $(TEST_BUILD_DIR) - echo 'TEST_LOAD_SOURCE = new' > $(TEST_BUILD_DIR)/dep.mk - -.PHONY: test-upgrade -set-test-upgrade: $(TEST_BUILD_DIR) - echo 'TEST_LOAD_SOURCE = upgrade' > $(TEST_BUILD_DIR)/dep.mk +# test/install/load.sql is the ONE place that installs everything the pgTAP +# suite depends on (the extension and the test roles + grants). pgxntool's +# native test/install feature runs it COMMITTED, before the suite, in its own +# pg_regress session; the state persists into every (rolled-back) test. So the +# per-test files no longer each reinstall -- a real time saver. We enable it in +# BOTH modes (install always happens via test/install), so it must be `yes` +# unconditionally here. +PGXNTOOL_ENABLE_TEST_INSTALL = yes +# +# TEST_LOAD_SOURCE selects how load.sql installs the extension: +# - fresh (default): CREATE EXTENSION cat_tools (current version). +# - update: CREATE EXTENSION at TEST_UPDATE_FROM (default 0.2.2, the +# backward-compat floor) then ALTER EXTENSION UPDATE -- to TEST_UPDATE_TO if +# set, otherwise to the current version. Running the SAME suite with the SAME +# expected output against the updated database verifies it behaves +# identically to a fresh install. +# - existing: the extension is ALREADY installed in the target database (by a +# binary pg_upgrade, or an ALTER EXTENSION UPDATE done outside the suite). +# load.sql does not touch it; it only asserts presence + current version and +# creates the test roles. Pair with CONTRIB_TESTDB= and +# EXTRA_REGRESS_OPTS=--use-existing so pg_regress runs against that database +# instead of dropping and recreating a throwaway one. +# +# The mode (and the update from/to versions) are signalled to load.sql by +# placeholder GUCs. pg_regress does not forward make variables, but the psql +# processes it spawns inherit the environment, so PGOPTIONS reaches load.sql. +# +# The GUCs are exported UNCONDITIONALLY, so load.sql can read them WITHOUT +# missing_ok and fail loudly if they did not propagate. Relying on an absent GUC +# to mean "fresh" is unsafe: a silent break anywhere in the +# make -> PGOPTIONS -> env -> psql chain would quietly run the wrong mode. +# +# TEST_LOAD_SOURCE must be exactly `fresh`, `update` or `existing`; anything else +# is a hard error at parse time (so e.g. `make test TEST_LOAD_SOURCE=typo` fails +# fast rather than defaulting). +# +# An update must be committed (which is why it lives in test/install, not in +# deps.sql's per-test transaction): the update to the current version runs +# ALTER TYPE ... ADD VALUE, and a newly added enum value cannot be USED in the +# transaction that added it (55P04). See test/install/load.sql. +# +# update mode requires PG12+ when it targets the current version (ALTER TYPE +# ... ADD VALUE cannot run in a transaction at all before PG12); CI restricts it +# accordingly. +TEST_LOAD_SOURCE ?= fresh +ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update existing),) +$(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)') +endif +# update-mode version range (read by load.sql only in update mode). Empty +# TEST_UPDATE_TO means "update to the current default_version". +TEST_UPDATE_FROM ?= 0.2.2 +TEST_UPDATE_TO ?= -$(TEST_BUILD_DIR)/active.sql: $(TEST_BUILD_DIR)/dep.mk $(TEST_BUILD_DIR)/$(TEST_LOAD_SOURCE).sql - ln -sf $(TEST_LOAD_SOURCE).sql $@ +export PGOPTIONS := $(PGOPTIONS) -c cat_tools.test_load_mode=$(TEST_LOAD_SOURCE) -c cat_tools.test_update_from=$(TEST_UPDATE_FROM) -c cat_tools.test_update_to=$(TEST_UPDATE_TO) -$(TEST_BUILD_DIR)/upgrade.sql: test/load_upgrade.sql $(TEST_BUILD_DIR) old_version - (echo @generated@ && cat $< && echo @generated@) | sed -e 's#@generated@#-- GENERATED FILE! DO NOT EDIT! See $<#' > $@.tmp - mv $@.tmp $@ +# Convenience wrapper: `make test-update` == `make test TEST_LOAD_SOURCE=update`. +# Must recurse (a fresh $(MAKE)) rather than depend on `test`, so the parse-time +# TEST_LOAD_SOURCE conditional above re-evaluates with update set. +.PHONY: test-update +test-update: + $(MAKE) test TEST_LOAD_SOURCE=update -$(TEST_BUILD_DIR)/new.sql: test/load_new.sql $(TEST_BUILD_DIR) - (echo @generated@ && cat $< && echo @generated@) | sed -e 's#@generated@#-- GENERATED FILE! DO NOT EDIT! See $<#' > $@.tmp - mv $@.tmp $@ +# Versioned SQL is generated from .sql.in at build time. That generation, the +# DATA list that installs it, and the relkind drift source all live in sql.mk, +# which also owns `include pgxntool/base.mk` (base.mk has no include guard, so it +# must be included exactly once; sql.mk includes it at its top). This Makefile +# therefore does NOT include base.mk itself -- only sql.mk. sql.mk documents the +# GNU Make two-phase (parse vs. recipe) hazards involved (e.g. +# https://github.com/Postgres-Extensions/cat_tools/issues/28) and relies on +# base.mk/control.mk/PGXS vars (EXTENSION_VERSION_FILES, PG_CONFIG, MAJORVER, +# datadir, ...). The PGXNTOOL_ENABLE_TEST_INSTALL / TEST_LOAD_SOURCE vars above +# are set before this include so base.mk (pulled in by sql.mk) sees them. +include sql.mk -# TODO: figure out vpath -EXTRA_CLEAN += $(TEST_BUILD_DIR)/ -$(TEST_BUILD_DIR): - [ -d $@ ] || mkdir -p $@ +# Clean the cruft pg_regress writes into test/install/ (the self-comparing +# result .out and its diff), which is listed in test/install/.gitignore. This is +# the pgxntool test/install feature configured above (not SQL generation), so +# its cleanup stays here rather than in sql.mk. +EXTRA_CLEAN += $(addprefix test/install/,$(shell grep -v '^\#' test/install/.gitignore 2>/dev/null)) .PHONY: old_version old_version: $(DESTDIR)$(datadir)/extension/cat_tools--0.2.0.sql diff --git a/README.asc b/README.asc index 6d2657d..70a4df0 100644 --- a/README.asc +++ b/README.asc @@ -16,9 +16,6 @@ it may break when you upgrade PostgreSQL. == Current Status -image:https://badge.fury.io/pg/cat_tools.svg["PGXN version",link="https://badge.fury.io/pg/cat_tools"] -image:https://travis-ci.org/decibel/cat_tools.png["Build Status",link="https://travis-ci.org/decibel/cat_tools"] - This is very much a work in progress. If it doesn't do something you need, please https://github.com/decibel/cat_tools/issues[open an issue]! === Supported Versions @@ -31,10 +28,14 @@ Works on Postgres 9.3 and above. * `constraint_type` - Types of constraints (`domain constraint` or `table_constraint`) * `relation_type` - Types of objects stored in `pg_class` * `relation_relkind` - Valid values for `pg_class.relkind` +* `routine_type` - Types of routines stored in `pg_proc` +* `routine_argument_mode` - Argument modes for function/procedure parameters +* `routine_volatility` - Volatility levels for functions/procedures +* `routine_parallel_safety` - Parallel safety levels for functions/procedures +* `routine_argument` - Detailed information about a single function/procedure argument -== Functions +== General Introspection Functions -* `currval(table, column)` - Returns current value for a sequence owned by a column * `enum_range(regtype)` - Returns valid values for an ENUM as an array * `enum_range_srf(regtype)` - Returns valid values for an ENUM as a recordset * `name__check(text)` - Throws an error if input would be truncated when cast to name @@ -42,19 +43,71 @@ Works on Postgres 9.3 and above. * `pg_extension__get(extension_name name)` - Returns pg_extension_v row for an extension * `extension__schemas(extension_names text/name[])` - Returns the schemas for the requested functions * `extension__schemas_unique(extension_names text/name[])` - Returns a unique array of schemas -* `function__arg_types(arguments)` - Accepts full function argument string and returns regtype[] of IN/INOUT arguments -* `function__arg_types_text(arguments)` - Version of `function__arg_types` that returns text +* `pg_attribute__get(relation regclass, column_name name)` - Returns `pg_attribute` row for a column; throws error if column doesn't exist +* `relation__column_names(relation regclass)` - Returns an array of quoted column names for a relation in ordinal position order +* `relation__is_catalog(relation regclass)` - Returns true if the relation is in the `pg_catalog` schema +* `relation__is_temp(relation regclass)` - Returns true if the relation is a temporary table (lives in a schema that starts with 'pg_temp') + +== Object Type Query Functions + +Functions for working with `object_type` values β€” the enum of descriptive names for every type of Postgres object (table, index, role, etc). + * `object__catalog(object_type)` - Returns catalog table that is used to store `object_type` objects * `object__reg_type(object_catalog)` - Returns the "reg" pseudotype (ie: regclass) associated with a system catalog (ie: pg_class) -* `regprocedure(function_name, arguments)` - Returns regprocedure for function_name and it's full set of arguments -* `relation__kind(relkind)` - Mapping from `pg_class.relkind` to a `relation_type` -* `relation__relkind(relation_type)` - Mapping from `relation_type` to a `pg_class.relkind` value +* `object__reg_type_catalog(object_identifier_type regtype)` - Returns the system catalog that stores a particular object identifier type (inverse of `object__reg_type`) +* `object__address_classid(object_type)` - Returns the classid used by the `pg_*_object*()` functions for an object_type +* `objects__shared()` - Returns an array of object types that are shared catalog objects (as opposed to per-database) +* `objects__shared_srf()` - Set returning version of `objects__shared` +* `object__is_shared(object_type)` - Returns true if object_type is a shared object +* `objects__address_unsupported()` - Returns array of object types not supported by `pg_get_object_address()` +* `objects__address_unsupported_srf()` - Set returning version of `objects__address_unsupported` +* `object__is_address_unsupported(object_type)` - Returns true if object type is not supported by `pg_get_object_address()` + +== Sequence Functions + +* `get_serial_sequence(table_name text, column_name text)` - Returns sequence associated with a column; unlike `pg_get_serial_sequence`, throws an exception if no sequence exists +* `currval(table_name text, column_name text)` - Alias for `sequence__last` +* `nextval(table_name text, column_name text)` - Alias for `sequence__next` +* `setval(table_name text, column_name text, new_value bigint, has_been_used boolean DEFAULT true)` - Sets sequence value for a column; if `has_been_used` is true, sequence will return `new_value + 1` next +* `sequence__last(table_name text, column_name text)` - Returns the last value assigned to a column's sequence +* `sequence__next(table_name text, column_name text)` - Returns the next sequence value and advances the sequence +* `sequence__set_last(table_name text, column_name text, last_value bigint)` - Sets last used value; sequence will return `last_value + 1` next +* `sequence__set_next(table_name text, column_name text, next_value bigint)` - Sets the next value the sequence will return + +== Routine / Function / Procedure Functions + +* `routine__parse_arg_types(arguments)` - Accepts full function argument string and returns regtype[] of IN/INOUT arguments +* `routine__parse_arg_types_text(arguments)` - Version of `routine__parse_arg_types` that returns text +* `routine__parse_arg_names(arguments)` - Accepts full function argument string and returns text[] of IN/INOUT argument names +* `routine__parse_arg_names_text(arguments)` - Version of `routine__parse_arg_names` that returns text +* `routine__arg_types(regprocedure)` - Returns argument types for a function as regtype[] +* `routine__arg_types_text(regprocedure)` - Version of `routine__arg_types` that returns text +* `routine__arg_names(regprocedure)` - Returns argument names for a function as text[] +* `routine__arg_names_text(regprocedure)` - Version of `routine__arg_names` that returns text +* `regprocedure(routine_name, arguments)` - Returns regprocedure for routine_name and its full set of arguments + +== Trigger Functions + * `trigger__args_as_text(text)` - Converts the arguments for a trigger function (as returned by `trigger__parse()`) to text (for backwards compatibility). * `trigger__get_oid(trigger_table, trigger_name)` - oid of a trigger. Throws error if trigger doesn't exits. * `trigger__get_oid__loose(trigger_table, trigger_name)` - oid of a trigger. Does _not_ throw error if trigger doesn't exits. * `trigger__parse(trigger oid)` - Returns information about a trigger * `trigger__parse(table_name regclass, trigger_name text)` - Returns information about a trigger +== Mapping Functions + +* `relation__kind(relkind)` - Mapping from `pg_class.relkind` to a `relation_type` +* `relation__relkind(relation_type)` - Mapping from `relation_type` to a `pg_class.relkind` value +* `routine__type(prokind)` - Mapping from `pg_proc.prokind` to `routine_type` +* `routine__argument_mode(mode)` - Mapping from `pg_proc.proargmodes` element to `routine_argument_mode` +* `routine__volatility(volatile)` - Mapping from `pg_proc.provolatile` to `routine_volatility` +* `routine__parallel_safety(parallel)` - Mapping from `pg_proc.proparallel` to `routine_parallel_safety` + +== Deprecated Functions + +* `function__arg_types(arguments)` - DEPRECATED: Use `routine__parse_arg_types` instead +* `function__arg_types_text(arguments)` - DEPRECATED: Use `routine__parse_arg_types_text` instead + == Views WARNING: These views may eventually move into a separate extension! 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/bin/test_existing b/bin/test_existing new file mode 100755 index 0000000..0d60561 --- /dev/null +++ b/bin/test_existing @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# +# Exercise the cat_tools test suite against a REAL database whose extension was +# installed/updated/pg_upgraded OUTSIDE the suite ("existing" mode). Both the +# pg-upgrade legs and the extension-update scenarios in .github/workflows/ci.yml +# repeat the same sequence: +# +# install -> plant dependency guard -> update/upgrade -> assert version +# -> run the suite in existing mode +# +# so it lives here once instead of being duplicated as inline YAML. It is NOT +# CI-only: a developer can run any subcommand locally against a scratch database. +# +# USAGE: bin/test_existing [args] +# +# plant-guard DB +# Plant + prove the dependency guard (extension must already be >= 0.2.2). +# +# update DB [TO_VERSION] +# ALTER EXTENSION cat_tools UPDATE [TO 'TO_VERSION'] (empty => current). +# +# prepare-old DB INSTALL_VERSION [BRIDGE_TO] +# Old-cluster prep for pg-upgrade-test: create DB + extension at +# INSTALL_VERSION, optionally bridge-update to BRIDGE_TO, then plant guard. +# +# run-suite DB +# Run the suite in existing mode (extension must be at the current version). +# +# update-scenario DB FROM_VERSION +# Create DB + extension at FROM_VERSION, plant guard, update to current, +# run the suite against that real updated database. +# +# update-check DB FROM_VERSION TO_VERSION +# Lightweight check that a specific update script applies (no suite). +# +# Run `bin/test_existing` with no subcommand to print usage. +# +# Why the dependency guard: "existing" mode must run the suite against the ACTUAL +# upgraded/updated objects. If anything silently dropped + reinstalled the +# extension, the suite would test a FRESH install and hide a regression. As +# belt-and-suspenders to the load.sql guarantee (existing mode never drops the +# extension), we plant an object that HARD-references a cat_tools member so a +# non-CASCADE DROP EXTENSION fails, and we actively PROVE that here (see +# plant_guard): if the drop unexpectedly succeeds, this script fails CI. +# +# See https://github.com/Postgres-Extensions/cat_tools/pull/16 for context. +set -euo pipefail + +# Run from the repository root (where `make` works and test paths resolve), +# regardless of the caller's cwd. bin/ sits directly under the repo root, so its +# parent is the root (no dependency on being inside a git checkout). readlink -f +# resolves any path the script was invoked through. +cd "$(dirname "$(readlink -f "$0")")/.." + +# A view whose output column has a cat_tools enum type creates a pg_depend edge +# to that extension member, so a non-CASCADE DROP EXTENSION cannot succeed. The +# enum is only ever extended (ALTER TYPE ... ADD VALUE) by the update scripts, +# never dropped, so the guard survives 0.2.2 -> current updates and binary +# pg_upgrade. +GUARD_SCHEMA=cat_tools_drop_guard +GUARD_VIEW=guard + +# --------------------------------------------------------------------------- +# psql helpers +# --------------------------------------------------------------------------- + +# Capture the single-value (-tAc) output of a query against a database. Used for +# the many "read one value back out" calls below so the psql flags live in one +# place. +psql_value() { + local db=$1 sql=$2 + psql -d "$db" -tAc "$sql" +} + +# Run SQL that must succeed, aborting the whole script on any error +# (ON_ERROR_STOP). Extra args pass through to psql, so callers use either +# `psql_do DB -c '...'` or a heredoc (`psql_do DB </dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p' +} + +installed_version() { + psql_value "$1" \ + "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'" +} + +guard_present() { + test "$(psql_value "$1" \ + "SELECT count(*) FROM pg_views WHERE schemaname = '$GUARD_SCHEMA' AND viewname = '$GUARD_VIEW'")" = 1 +} + +# Plant the guard and PROVE it blocks a non-CASCADE drop. Call right after +# CREATE EXTENSION (and before any update/upgrade) so it persists through them. +plant_guard() { + local db=$1 + psql_do "$db" </dev/null 2>&1; then + echo "FAIL: DROP EXTENSION cat_tools (non-CASCADE) unexpectedly SUCCEEDED in '$db' -- dependency guard is ineffective" >&2 + exit 1 + fi + guard_present "$db" \ + || { echo "FAIL: dependency guard missing from '$db' after the drop attempt" >&2; exit 1; } + test -n "$(installed_version "$db")" \ + || { echo "FAIL: cat_tools extension missing from '$db' after the drop attempt" >&2; exit 1; } + echo "OK: non-CASCADE DROP EXTENSION is blocked in '$db' (dependency guard effective)" +} + +assert_version() { + local db=$1 expected=$2 installed + [ "$expected" = current ] && expected=$(current_version) + installed=$(installed_version "$db") + echo "version check '$db': installed='$installed' expected='$expected'" + if [ -z "$installed" ] || [ -z "$expected" ] || [ "$installed" != "$expected" ]; then + echo "FAIL: cat_tools in '$db' is '$installed', expected '$expected'" >&2 + exit 1 + fi +} + +update_ext() { + local db=$1 to=${2:-} + # Prepend "TO " only when a target version is given, so a single statement + # covers both cases (empty $to => bare "ALTER EXTENSION ... UPDATE" to current). + # Use `if`, not `&&`: a false test under `set -e` would abort the script. + if [ -n "$to" ]; then to="TO '$to'"; fi + psql_do "$db" -c "ALTER EXTENSION cat_tools UPDATE $to" +} + +# --------------------------------------------------------------------------- +# Subcommand implementations +# --------------------------------------------------------------------------- + +# prepare-old DB INSTALL_VERSION [BRIDGE_TO] +# Old-cluster preparation for pg-upgrade-test. Create the database and the +# extension at INSTALL_VERSION; if BRIDGE_TO is given, ALTER EXTENSION UPDATE +# TO it first. That "bridge" models the real migration path a user on an OLD +# PostgreSQL + OLD cat_tools must take: e.g. on PG10, install 0.2.0 then update +# to 0.2.3 BEFORE pg_upgrade. The shipped 0.2.0->0.2.2 / 0.2.1->0.2.2 scripts do +# NOT fix the views (their omit_column used the no-op `!= ANY`, leaving +# relhasoids/relhaspkey in _cat_tools.pg_class_v); the pg_class_v DROP+CREATE +# rebuild that strips those columns lives in the 0.2.2->0.2.3 update, so the +# bridge must reach 0.2.3. The raw 0.2.0 views reference catalog columns removed +# in newer PostgreSQL and would break binary pg_upgrade otherwise. 0.2.3 is also +# the furthest a PG10 cluster can reach (0.2.3->0.3.0 uses ALTER TYPE ... ADD +# VALUE, unrunnable in an update script before PG12). Then plant + prove the +# dependency guard (at the bridged version, so cat_tools.relation_type exists). +prepare_old() { + local db=$1 install=$2 bridge=${3:-} + createdb "$db" + psql_do "$db" -c "CREATE EXTENSION cat_tools VERSION '$install'" + # Use `if`, not `&&`: under `set -e` a false `[ -n ... ] && ...` would abort. + if [ -n "$bridge" ]; then update_ext "$db" "$bridge"; fi + plant_guard "$db" +} + +# update-scenario DB FROM_VERSION +# Full extension-update flow that runs the existing-mode suite: create the DB +# and extension at FROM_VERSION, plant + prove the guard, update to the current +# version, then run the suite against that real updated database. +update_scenario() { + local db=$1 from=$2 + createdb "$db" + psql_do "$db" -c "CREATE EXTENSION cat_tools VERSION '$from'" + plant_guard "$db" + update_ext "$db" + run_suite "$db" +} + +# update-check DB FROM_VERSION TO_VERSION +# Lightweight check that a specific update script applies (no suite): used for +# the pre-0.2.2 scripts, which only install on PG10 and target 0.2.2 (not the +# current version, so the suite cannot run against them). +update_check() { + local db=$1 from=$2 to=$3 + createdb "$db" + psql_do "$db" -c "CREATE EXTENSION cat_tools VERSION '$from'" + update_ext "$db" "$to" + assert_version "$db" "$to" +} + +# Run the pgTAP suite against an already-populated database in existing mode. +# Verifies the extension is at the current version, re-proves the guard still +# blocks a drop (i.e. it survived the update/upgrade), runs the suite via +# --use-existing so pg_regress does NOT drop/recreate the database, then confirms +# the guard is still present (a CASCADE drop+reinstall would have removed it). +run_suite() { + local db=$1 + assert_version "$db" current + assert_drop_blocked "$db" + make check-relkind-source + # In existing mode pg_regress runs against $db via --use-existing and must NOT + # create/drop its own database. Two consequences drive the make args below: + # 1. PGXNTOOL_ENABLE_TEST_BUILD=no: base.mk auto-enables the test-build sanity + # check whenever test/build/*.sql exist, adding it as a `test` prerequisite. + # test-build spawns a recursive `installcheck` that INHERITS this call's + # --use-existing (a command-line var propagates to sub-makes) but targets a + # fresh `regression` DB it cannot create under --use-existing, so it dies + # with "database regression does not exist". test-build is a fresh-install + # check already run by the fresh `test` job on every PG version, so it adds + # nothing here -- disable it. + # 2. verify-results depends on `test`, so it re-runs the suite; it must carry + # the SAME existing-mode overrides or it would re-run FRESH (against a new + # regression DB) instead of verifying THIS existing database. + local existing_args="TEST_LOAD_SOURCE=existing CONTRIB_TESTDB=$db EXTRA_REGRESS_OPTS=--use-existing PGXNTOOL_ENABLE_TEST_BUILD=no" + make test $existing_args + make verify-results $existing_args + # Post-suite guard assertion: a CASCADE drop+reinstall during the run would + # have removed the guard view, meaning the suite tested a fresh install, not $db. + guard_present "$db" \ + || { echo "FAIL: dependency guard vanished during the suite run on '$db' -- extension was dropped+reinstalled (CASCADE)?" >&2; exit 1; } +} + +usage() { + echo "usage: bin/test_existing [args]" >&2 + echo " plant-guard DB" >&2 + echo " update DB [TO_VERSION]" >&2 + echo " prepare-old DB INSTALL_VERSION [BRIDGE_TO]" >&2 + echo " run-suite DB" >&2 + echo " update-scenario DB FROM_VERSION" >&2 + echo " update-check DB FROM_VERSION TO_VERSION" >&2 + exit 2 +} + +# Explicit subcommand dispatch on $1. Defined first for readability; INVOKED at +# the very bottom, after every helper it calls is defined (bash resolves calls at +# runtime, so main() appearing first is fine). +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + plant-guard) plant_guard "$@" ;; + update) update_ext "$@" ;; + prepare-old) prepare_old "$@" ;; + run-suite) run_suite "$@" ;; + update-scenario) update_scenario "$@" ;; + update-check) update_check "$@" ;; + *) usage ;; + esac +} + +main "$@" diff --git a/cat_tools.control b/cat_tools.control index 6a6297a..d798380 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.3.0' relocatable = false schema = 'cat_tools' diff --git a/control.mk b/control.mk index 9ac83c2..26db2f6 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.3.0 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.mk b/sql.mk index e2f673d..bd079ef 100644 --- a/sql.mk +++ b/sql.mk @@ -80,6 +80,85 @@ all: sql/cat_tools.sql $(versioned_out) installcheck: sql/cat_tools.sql $(versioned_out) EXTRA_CLEAN += sql/cat_tools.sql $(versioned_out) +# +# relkind drift source generation +# +# Generate the canonical set of pg_class.relkind values from the server headers +# we are building against, for the relkind drift check in test/sql/relation__.sql. +# The output is gitignored (per-version). When postgresql-server-dev-NN is not +# installed the script emits an empty view so `make test` still works locally; +# CI runs check-relkind-source (below) so a missing header can never let the +# drift check pass silently, and `make test` warns about it locally. +RELKIND_SRC = test/.generated/pg_class_relkinds.sql + +# Absolute path to the header we extract relkinds from. Computed each `make`; +# it changes when we build against a different PostgreSQL (pg_config differs). +RELKIND_HEADER := $(shell $(PG_CONFIG) --includedir-server)/catalog/pg_class.h + +# Stamp recording the header path. FORCE runs the recipe every time, but it only +# rewrites the stamp (bumping its mtime) when the path actually changed, so the +# source is regenerated after a PostgreSQL-version switch even though the old +# header file itself is untouched. This is the same "name-derive the source" +# trick as versioned_out, but for a source (a server header) that lives OUTSIDE +# the tree: we cannot glob it into the dependency graph reliably, so we track its +# PATH in a committed-shaped stamp file instead. +RELKIND_STAMP = test/.generated/.relkind-header-path + +# Directory the generated files live in. It is listed as an ORDER-ONLY +# prerequisite (after `|`) on the stamp and source recipes below: the directory +# must exist before we write into it, but we must NOT rebuild those files just +# because the directory changed. A directory's mtime bumps every time a file is +# added to or removed from it, so as a normal prerequisite it would force +# needless regeneration on every run; after `|` it means "ensure it exists, but +# its timestamp is not a rebuild trigger." +test/.generated: + @mkdir -p $@ + +.PHONY: FORCE +FORCE: + +$(RELKIND_STAMP): FORCE | test/.generated + @echo '$(RELKIND_HEADER)' | cmp -s - "$@" 2>/dev/null || echo '$(RELKIND_HEADER)' > "$@" + +# Real file target (not .PHONY): regenerate only when the generator, the header +# file, or the header path change -- not on every `make`. $(wildcard ...) yields +# no prereq (rather than an error) when the header is absent, in which case +# gen-relkinds.sh emits an empty view. +$(RELKIND_SRC): test/gen-relkinds.sh $(RELKIND_STAMP) $(wildcard $(RELKIND_HEADER)) | test/.generated + test/gen-relkinds.sh "$(RELKIND_HEADER)" > "$@" + +.PHONY: gen-relkinds +gen-relkinds: $(RELKIND_SRC) + +testdeps: $(RELKIND_SRC) +EXTRA_CLEAN += $(RELKIND_SRC) $(RELKIND_STAMP) + +# Guard for CI: fail if the relkind source has no relkinds (server headers +# missing), so the drift check in test/sql/relation__.sql cannot silently pass +# on an empty view. CI runs `make check-relkind-source` before `make test` on +# every PostgreSQL version; local `make test` stays lenient (see +# warn-relkind-source). +.PHONY: check-relkind-source +check-relkind-source: $(RELKIND_SRC) + @grep -q 'RELKIND_' $(RELKIND_SRC) || { \ + echo "ERROR: PostgreSQL catalog header not found at"; \ + echo " $(RELKIND_HEADER)"; \ + echo " so the relkind drift check in test/sql/relation__.sql would"; \ + echo " pass without running. Install this PostgreSQL version's server"; \ + echo " development headers so that path exists."; \ + exit 1; \ + } + @echo "check-relkind-source: $(RELKIND_SRC) is populated" + +# Non-fatal counterpart, run at the end of `make test`: warn (do not fail) when +# the drift source is empty because the server headers are missing, so a local +# run without postgresql-server-dev-NN makes clear the drift check did not run. +# Listed as a `test` prerequisite after base.mk's, so it runs once tests are done. +.PHONY: warn-relkind-source +warn-relkind-source: $(RELKIND_SRC) + @grep -q 'RELKIND_' $(RELKIND_SRC) || echo "WARNING: PostgreSQL catalog header not found at $(RELKIND_HEADER); the relkind drift check in test/sql/relation__.sql did NOT run. Install this PostgreSQL version's server development headers to enable it." +test: warn-relkind-source + # # Versioned-SQL generation from .sql.in # 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..86585a7 --- /dev/null +++ b/sql/cat_tools--0.2.2--0.2.3.sql.in @@ -0,0 +1,206 @@ +/* + * 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.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +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$; + +/* + * REPAIR: force a full rebuild of the _cat_tools.pg_class_v dependency chain. + * + * The published 0.2.2 could be reached via the 0.2.0->0.2.2 / 0.2.1->0.2.2 + * update scripts, whose __cat_tools.omit_column used `!= ANY(omit)` -- a no-op + * for a multi-element omit list, so relhasoids/relhaspkey were never stripped + * from _cat_tools.pg_class_v. Such a database, once binary-pg_upgraded to PG12+ + * (where pg_class has no relhasoids/relhaspkey), holds a view that references + * non-existent catalog columns. Rebuild the chain here so an already-broken + * 0.2.2 database is REPAIRED when it updates to 0.2.3. Where the view is already + * correct (fresh 0.2.2 install, or a PG major where those columns never + * existed) this is a harmless no-op rebuild. + * + * CREATE OR REPLACE VIEW cannot drop columns, so drop the chain in reverse + * dependency order (no CASCADE, so a DROP fails loudly on a user dependency) and + * recreate it forward with the corrected column list. cat_tools.pg_class() is + * dropped because it RETURNS the cat_tools.pg_class_v rowtype. + */ +DROP VIEW cat_tools.column; +DROP VIEW _cat_tools.column; +DROP VIEW _cat_tools.pg_attribute_v; +DROP FUNCTION cat_tools.pg_class(pg_catalog.regclass); +DROP VIEW cat_tools.pg_class_v; +DROP VIEW _cat_tools.pg_class_v; + +SELECT __cat_tools.exec(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']) +)); +REVOKE ALL ON _cat_tools.pg_class_v FROM public; + +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' ) +; +GRANT SELECT ON cat_tools.pg_class_v TO cat_tools__usage; + +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$; +REVOKE ALL ON FUNCTION cat_tools.pg_class(pg_catalog.regclass) FROM public; +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. + */ +SELECT __cat_tools.exec(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') +)); +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. + */ +SELECT __cat_tools.exec(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') +)); +REVOKE ALL ON _cat_tools.column FROM public; + +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 +; +GRANT SELECT ON cat_tools.column TO cat_tools__usage; + +-- Drop temporary helper objects +DROP FUNCTION __cat_tools.omit_column( + rel text + , omit name[] +); +DROP FUNCTION __cat_tools.exec( + sql text +); +DROP SCHEMA __cat_tools; + +/* + * 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--0.2.3--0.3.0.sql.in b/sql/cat_tools--0.2.3--0.3.0.sql.in new file mode 100644 index 0000000..86d9bd2 --- /dev/null +++ b/sql/cat_tools--0.2.3--0.3.0.sql.in @@ -0,0 +1,870 @@ +CREATE SCHEMA __cat_tools; + +CREATE FUNCTION __cat_tools.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $body$ +BEGIN + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text DEFAULT NULL + , comment text DEFAULT NULL +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); + + create_template CONSTANT text := $template$ +CREATE OR REPLACE FUNCTION %s( +%s +) RETURNS %s AS +%L +$template$ + ; + + revoke_template CONSTANT text := $template$ +REVOKE ALL ON FUNCTION %s( +%s +) FROM public; +$template$ + ; + + grant_template CONSTANT text := $template$ +GRANT EXECUTE ON FUNCTION %s( +%s +) TO %s; +$template$ + ; + + comment_template CONSTANT text := $template$ +COMMENT ON FUNCTION %s( +%s +) IS %L; +$template$ + ; + +BEGIN + PERFORM __cat_tools.exec( format( + create_template + , function_name + , args + , options -- TODO: Force search_path if options ~* 'definer' + , body + ) ) + ; + PERFORM __cat_tools.exec( format( + revoke_template + , function_name + , c_simple_args + ) ) + ; + + IF grants IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + grant_template + , function_name + , c_simple_args + , grants + ) ) + ; + END IF; + + IF comment IS NOT NULL THEN + PERFORM __cat_tools.exec( format( + comment_template + , function_name + , c_simple_args + , comment + ) ) + ; + END IF; +END +$body$; + + +-- NOTE: Changes already applied on the path up to 0.2.3: +-- ALTER DEFAULT PRIVILEGES, pg_class_v fix, pg_attribute_v fix, pg_extension_v fix, +-- pg_extension__get recreation, cat_tools.column recreation are all skipped here. +-- They land in 0.2.2 (fresh install) / the 0.2.2β†’0.2.3 rebuild, so a database +-- reaching 0.2.3 by any route already has them. + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L165 +SELECT __cat_tools.create_function( + '_cat_tools.function__arg_to_regprocedure' + , 'arguments text, function_suffix text, api_function_name text' + , 'pg_catalog.regprocedure LANGUAGE plpgsql' + , $body$ +DECLARE + c_template CONSTANT text := $fmt$CREATE FUNCTION pg_temp.cat_tools__function__%s__temp_function( + %s + ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN RETURN; END' + $fmt$; + + temp_proc pg_catalog.regprocedure; + sql text; +BEGIN + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('current_user is %s, session_user is %s', current_user, session_user) + , HINT = 'Helper functions must not be called from SECURITY DEFINER context.'; + END IF; + sql := format( + c_template + , function_suffix + , arguments + , 'void' + ); + 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 + , function_suffix + , arguments + , v_type + ); + EXECUTE sql; + END; + + EXECUTE format( + $$SELECT 'pg_temp.cat_tools__function__%s__temp_function'::pg_catalog.regproc::pg_catalog.regprocedure$$ + , function_suffix + ) INTO temp_proc; + + RETURN temp_proc; +END +$body$ + , 'cat_tools__usage' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L233 +SELECT __cat_tools.create_function( + '_cat_tools.function__drop_temp' + , 'p_regprocedure pg_catalog.regprocedure, api_function_name text' + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('API function %s must not be called from a SECURITY DEFINER function', api_function_name) + , HINT = 'We detect SECURITY DEFINER context by comparing current_user and session_user, which can cause false positives if SET ROLE is used'; + END IF; + + EXECUTE 'DROP ROUTINE ' || p_regprocedure; +END +$body$ + , 'cat_tools__usage' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L254 +/* + * Both helpers above are created via __cat_tools.create_function, which applies + * the REVOKE-from-PUBLIC + GRANT-to-cat_tools__usage automatically (and keeps it + * in sync with the fresh-install script). Only the schema USAGE grant remains. + */ +GRANT USAGE ON SCHEMA _cat_tools TO cat_tools__usage; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L280 +ALTER TYPE cat_tools.relation_type ADD VALUE 'partitioned table'; +ALTER TYPE cat_tools.relation_type ADD VALUE 'partitioned index'; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L294 +ALTER TYPE cat_tools.relation_relkind ADD VALUE 'p'; +ALTER TYPE cat_tools.relation_relkind ADD VALUE 'I'; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L299 +CREATE TYPE cat_tools.routine_prokind AS ENUM( + 'f' -- function + , 'p' -- procedure + , 'a' -- aggregate + , 'w' -- window +); +COMMENT ON TYPE cat_tools.routine_prokind IS $$Valid values for `pg_proc.prokind`$$; + +CREATE TYPE cat_tools.routine_type AS ENUM( + 'function' + , 'procedure' + , 'aggregate' + , 'window' +); +COMMENT ON TYPE cat_tools.routine_type IS $$Types of routines stored in `pg_proc`$$; + +CREATE TYPE cat_tools.routine_proargmode AS ENUM( + 'i' -- in + , 'o' -- out + , 'b' -- inout + , 'v' -- variadic + , 't' -- table +); +COMMENT ON TYPE cat_tools.routine_proargmode IS $$Valid values for `pg_proc.proargmodes` elements$$; + +CREATE TYPE cat_tools.routine_argument_mode AS ENUM( + 'in' + , 'out' + , 'inout' + , 'variadic' + , 'table' +); +COMMENT ON TYPE cat_tools.routine_argument_mode IS $$Argument modes for function/procedure parameters$$; + +CREATE TYPE cat_tools.routine_provolatile AS ENUM( + 'i' -- immutable + , 's' -- stable + , 'v' -- volatile +); +COMMENT ON TYPE cat_tools.routine_provolatile IS $$Valid values for `pg_proc.provolatile`$$; + +CREATE TYPE cat_tools.routine_volatility AS ENUM( + 'immutable' + , 'stable' + , 'volatile' +); +COMMENT ON TYPE cat_tools.routine_volatility IS $$Volatility levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_proparallel AS ENUM( + 's' -- safe + , 'r' -- restricted + , 'u' -- unsafe +); +COMMENT ON TYPE cat_tools.routine_proparallel IS $$Valid values for `pg_proc.proparallel`$$; + +CREATE TYPE cat_tools.routine_parallel_safety AS ENUM( + 'safe' + , 'restricted' + , 'unsafe' +); +COMMENT ON TYPE cat_tools.routine_parallel_safety IS $$Parallel safety levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_argument AS ( + argument_name text + , argument_type pg_catalog.regtype + , argument_mode cat_tools.routine_argument_mode + , argument_default text +); +COMMENT ON TYPE cat_tools.routine_argument IS $$Detailed information about a single function/procedure argument$$; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L372 +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind cat_tools.relation_relkind' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* + * The c/f/m arms were previously mapped backwards (c->materialized view, + * f->composite type, m->foreign table), disagreeing with pg_class.relkind + * semantics documented in src/include/catalog/pg_class.h. Correct mapping is + * 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' + WHEN 'p' THEN 'partitioned table' + WHEN 'I' THEN 'partitioned index' +END::cat_tools.relation_type +$body$ + , 'cat_tools__usage' + , 'Mapping from to a ' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L394 +SELECT __cat_tools.create_function( + 'cat_tools.relation__relkind' + , 'kind cat_tools.relation_type' + , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* + * The composite type / foreign table / materialized view arms were previously + * mapped backwards (composite type->f, foreign table->m, materialized view->c), + * disagreeing with pg_class.relkind semantics documented in + * src/include/catalog/pg_class.h. Correct mapping is composite type=c, + * foreign table=f, materialized view=m. + */ +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' + WHEN 'partitioned table' THEN 'p' + WHEN 'partitioned index' THEN 'I' +END::cat_tools.relation_relkind +$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L416 +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' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L425 +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' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L434 +SELECT __cat_tools.create_function( + 'cat_tools.routine__type' + , 'prokind cat_tools.routine_prokind' + , 'cat_tools.routine_type LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE prokind + WHEN 'f' THEN 'function' + WHEN 'p' THEN 'procedure' + WHEN 'a' THEN 'aggregate' + WHEN 'w' THEN 'window' +END::cat_tools.routine_type +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_prokind to cat_tools.routine_type' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L459 +CREATE CAST ("char" AS cat_tools.routine_prokind) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proargmode) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_provolatile) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proparallel) WITH INOUT AS IMPLICIT; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L463 +SELECT __cat_tools.create_function( + 'cat_tools.routine__argument_mode' + , 'proargmode cat_tools.routine_proargmode' + , 'cat_tools.routine_argument_mode LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proargmode + WHEN 'i' THEN 'in' + WHEN 'o' THEN 'out' + WHEN 'b' THEN 'inout' + WHEN 'v' THEN 'variadic' + WHEN 't' THEN 'table' +END::cat_tools.routine_argument_mode +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proargmode to cat_tools.routine_argument_mode' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__volatility' + , 'provolatile cat_tools.routine_provolatile' + , 'cat_tools.routine_volatility LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE provolatile + WHEN 'i' THEN 'immutable' + WHEN 's' THEN 'stable' + WHEN 'v' THEN 'volatile' +END::cat_tools.routine_volatility +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_provolatile to cat_tools.routine_volatility' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parallel_safety' + , 'proparallel cat_tools.routine_proparallel' + , 'cat_tools.routine_parallel_safety LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proparallel + WHEN 's' THEN 'safe' + WHEN 'r' THEN 'restricted' + WHEN 'u' THEN 'unsafe' +END::cat_tools.routine_parallel_safety +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proparallel to cat_tools.routine_parallel_safety' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L502 +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types' + , $$func pg_catalog.regprocedure$$ + , $$pg_catalog.regtype[] LANGUAGE sql STABLE$$ + , $body$ +SELECT string_to_array(proargtypes::text,' ')::pg_catalog.regtype[] +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as an array of regtype' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L519 +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names' + , $$func pg_catalog.regprocedure$$ + , $$text[] LANGUAGE sql STABLE$$ + , $body$ +SELECT + CASE + WHEN proargnames IS NULL THEN + CASE + WHEN pronargs > 0 THEN + array_fill(NULL::text, ARRAY[pronargs]) + ELSE + '{}'::text[] + END + WHEN proargmodes IS NULL THEN + array( + SELECT CASE WHEN name = '' THEN NULL ELSE name END + FROM unnest(proargnames) AS name + ) + ELSE + array( + SELECT + CASE + WHEN i <= array_length(proargnames, 1) AND proargnames[i] != '' THEN proargnames[i] + ELSE NULL + END + FROM unnest(proargmodes) WITH ORDINALITY AS t(mode, i) + WHERE mode IN ('i', 'b', 'v') + ) + END +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as an array of text. Empty strings are converted to NULL.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L561 +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_types($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as a comma-separated text string' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L574 +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as a comma-separated text string' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L587 +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_types', 'cat_tools.routine__parse_arg_types'); + result pg_catalog.regtype[]; +BEGIN + result := cat_tools.routine__arg_types(c_temp_proc); + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_types'); + RETURN result; +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.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L612 +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_names' + , $$arguments text$$ + , $$text[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_names', 'cat_tools.routine__parse_arg_names'); + result text[]; +BEGIN + result := cat_tools.routine__arg_names(c_temp_proc); + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_names'); + RETURN result; +END +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as an array. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types + behavior). Unnamed arguments appear as NULL in the result array.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L637 +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.routine__parse_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.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L653 +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_names_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.routine__parse_arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types_text + behavior). Unnamed arguments appear as empty strings in the result.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L670 +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types() is deprecated, use routine__parse_arg_types instead'; + RETURN cat_tools.routine__parse_arg_types(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types instead. + Returns argument types for a function argument body as regtype[]. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L689 +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead'; + RETURN cat_tools.routine__parse_arg_types_text(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types_text instead. + Returns argument types for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L708 +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.routine__parse_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.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L742 +ALTER TYPE cat_tools.object_type ADD VALUE 'partitioned table' AFTER 'foreign table'; +ALTER TYPE cat_tools.object_type ADD VALUE 'partitioned index' AFTER 'partitioned table'; + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L890 +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' + , 'partitioned table' + , 'partitioned index' + ]::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 + 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' + 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' +); + + + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L1202 +SELECT __cat_tools.create_function( + '_cat_tools._pg_sv_column_array' + , 'OID, SMALLINT[]' + , 'NAME[] LANGUAGE sql STABLE' + , $$ + SELECT ARRAY( + SELECT a.attname + FROM unnest($2) WITH ORDINALITY AS t(attnum, i) + JOIN pg_catalog.pg_attribute a ON a.attnum = t.attnum + WHERE attrelid = $1 + ORDER BY i + ) +$$ +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L1582 +SELECT __cat_tools.create_function( + 'cat_tools.relation__is_temp' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text ~ '^pg_temp' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , $$Returns true if the relation is a temporary table (lives in a schema that starts with 'pg_temp').$$ +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L1597 +SELECT __cat_tools.create_function( + 'cat_tools.relation__is_catalog' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text = 'pg_catalog' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , 'Returns true if the relation is in the pg_catalog schema.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L1610 +SELECT __cat_tools.create_function( + 'cat_tools.relation__column_names' + , 'relation pg_catalog.regclass' + , $$text[] LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT array_agg(quote_ident(attname) ORDER BY attnum) +FROM pg_catalog.pg_attribute +WHERE attrelid = $1 + AND attnum > 0 + AND NOT attisdropped +$body$ + , 'cat_tools__usage' + , 'Returns an array of quoted column names for a relation in ordinal position order.' +); + +-- https://github.com/jnasbyupgrade/cat_tools/blob/new_functions/sql/cat_tools.sql.in#L1702 +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 + -- Use a generic pattern rather than the regproc name, since pg_get_triggerdef + -- may render temp functions as "pg_temp.f" while ::regproc gives "pg_temp_N.f". + v_execute_clause := E' EXECUTE (FUNCTION|PROCEDURE) \\S+\\('; + v_array := regexp_split_to_array( v_work, v_execute_clause ); + EXECUTE format( + CASE WHEN coalesce( rtrim( v_array[2], ')' ), '' ) = '' + THEN 'SELECT ARRAY[]::text[]' + ELSE 'SELECT array[ %s ]' + END + , rtrim( v_array[2], ')' ) -- Yank trailing ) + ) + INTO function_arguments + ; + RAISE DEBUG 'v_array[2] "%"', v_array[2]; + -- Get everything prior to EXECUTE PROCEDURE ... + v_work := v_array[1]; + RAISE DEBUG 'v_work "%"', v_work; + + row_statement := (regexp_matches( v_work, 'FOR EACH (ROW|STATEMENT)' ))[1]; + + -- Get [ NOT DEFERRABLE | [ DEFERRABLE ] { INITIALLY IMMEDIATE | INITIALLY DEFERRED } ] + v_array := regexp_split_to_array( v_work, 'FOR EACH (ROW|STATEMENT)' ); + RAISE DEBUG 'v_work = "%", v_array = "%"', v_work, v_array; + defer := rtrim(v_array[1]); + + IF r_trigger.tgqual IS NOT NULL THEN + when_clause := rtrim( + (regexp_split_to_array( v_array[2], E' WHEN \\(' ))[2] + , ')' + ); + END IF; + + RAISE DEBUG +$$v_create_stanza = "%" + v_on_clause = "%" + v_execute_clause = "%"$$ + , v_create_stanza + , v_on_clause + , v_execute_clause + ; + + RETURN; +END +$body$ + , 'cat_tools__usage' + , 'Provide details about a trigger.' +); + + +DROP FUNCTION __cat_tools.exec( + sql text +); +DROP FUNCTION __cat_tools.create_function( + function_name text + , args text + , options text + , body text + , grants text + , comment text +); +DROP SCHEMA __cat_tools; diff --git a/sql/cat_tools--0.3.0.sql.in b/sql/cat_tools--0.3.0.sql.in new file mode 100644 index 0000000..23ed69c --- /dev/null +++ b/sql/cat_tools--0.3.0.sql.in @@ -0,0 +1,1980 @@ +@generated@ + +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.routine__parse_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.routine__parse_arg_types_text(args); + + create_template CONSTANT text := $template$ +CREATE OR REPLACE FUNCTION %s( +%s +) RETURNS %s AS +%L +$template$ + ; + + revoke_template CONSTANT text := $template$ +REVOKE ALL ON FUNCTION %s( +%s +) FROM public; +$template$ + ; + + grant_template CONSTANT text := $template$ +GRANT EXECUTE ON FUNCTION %s( +%s +) TO %s; +$template$ + ; + + comment_template CONSTANT text := $template$ +COMMENT ON FUNCTION %s( +%s +) IS %L; +$template$ + ; + +@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@ + +/* + * These _cat_tools helper functions are created via __cat_tools.create_function + * so that the REVOKE-from-PUBLIC + GRANT-to-cat_tools__usage is applied + * uniformly and cannot drift from the update script (a hand-written REVOKE was + * previously forgotten there). At this point cat_tools.routine__parse_arg_types_text + * is still the pass-through stub, so create_function's generated REVOKE/GRANT + * carry the argument names rather than bare types; that is harmless because + * PostgreSQL ignores argument names when matching a function signature. + */ +SELECT __cat_tools.create_function( + '_cat_tools.function__arg_to_regprocedure' + , 'arguments text, function_suffix text, api_function_name text' + , 'pg_catalog.regprocedure LANGUAGE plpgsql' + , $body$ +DECLARE + /* + * Template for creating a temporary function with the user-provided argument + * signature. This allows us to leverage PostgreSQL's parser to validate and + * extract argument information without permanently creating a function. + * Using plpgsql language for the temp function to handle any return type. + */ + c_template CONSTANT text := $fmt$CREATE FUNCTION pg_temp.cat_tools__function__%s__temp_function( + %s + ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN RETURN; END' + $fmt$; + + temp_proc pg_catalog.regprocedure; + sql text; +BEGIN + /* + * Security check: Ensure current_user == session_user to detect SECURITY DEFINER context + * This prevents SQL injection attacks through elevated privileges. + */ + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' /* invalid_authorization_specification */ + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('current_user is %s, session_user is %s', current_user, session_user) + , HINT = 'Helper functions must not be called from SECURITY DEFINER context.'; + END IF; + sql := format( + c_template + , function_suffix + , 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 + , function_suffix + , 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 format( + $$SELECT 'pg_temp.cat_tools__function__%s__temp_function'::pg_catalog.regproc::pg_catalog.regprocedure$$ + , function_suffix + ) INTO temp_proc; + + RETURN temp_proc; +END +$body$ + , 'cat_tools__usage' +); + +SELECT __cat_tools.create_function( + '_cat_tools.function__drop_temp' + , 'p_regprocedure pg_catalog.regprocedure, api_function_name text' + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + /* + * Security check: Ensure current_user == session_user to detect SECURITY DEFINER context + * This prevents SQL injection attacks through elevated privileges. + */ + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' /* invalid_authorization_specification */ + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('API function %s must not be called from a SECURITY DEFINER function', api_function_name) + , HINT = 'We detect SECURITY DEFINER context by comparing current_user and session_user, which can cause false positives if SET ROLE is used'; + END IF; + + EXECUTE 'DROP ROUTINE ' || p_regprocedure; +END +$body$ + , 'cat_tools__usage' +); + +GRANT USAGE ON SCHEMA _cat_tools TO cat_tools__usage; + +@generated@ + +-- Data type definitions +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' + , 'partitioned table' + , 'partitioned index' +); +COMMENT ON TYPE cat_tools.relation_type IS $$Types of objects stored in `pg_class`$$; + +CREATE TYPE cat_tools.relation_relkind AS ENUM( + 'r' -- table + , 'i' -- index + , 'S' -- sequence + , 't' -- toast table + , 'v' -- view + , 'c' -- composite type + , 'f' -- foreign table + , 'm' -- materialized view + , 'p' -- partitioned table + , 'I' -- partitioned index +); +COMMENT ON TYPE cat_tools.relation_relkind IS $$Valid values for `pg_class.relkind`$$; + +CREATE TYPE cat_tools.routine_prokind AS ENUM( + 'f' -- function + , 'p' -- procedure + , 'a' -- aggregate + , 'w' -- window +); +COMMENT ON TYPE cat_tools.routine_prokind IS $$Valid values for `pg_proc.prokind`$$; + +CREATE TYPE cat_tools.routine_type AS ENUM( + 'function' + , 'procedure' + , 'aggregate' + , 'window' +); +COMMENT ON TYPE cat_tools.routine_type IS $$Types of routines stored in `pg_proc`$$; + +CREATE TYPE cat_tools.routine_proargmode AS ENUM( + 'i' -- in + , 'o' -- out + , 'b' -- inout + , 'v' -- variadic + , 't' -- table +); +COMMENT ON TYPE cat_tools.routine_proargmode IS $$Valid values for `pg_proc.proargmodes` elements$$; + +CREATE TYPE cat_tools.routine_argument_mode AS ENUM( + 'in' + , 'out' + , 'inout' + , 'variadic' + , 'table' +); +COMMENT ON TYPE cat_tools.routine_argument_mode IS $$Argument modes for function/procedure parameters$$; + +CREATE TYPE cat_tools.routine_provolatile AS ENUM( + 'i' -- immutable + , 's' -- stable + , 'v' -- volatile +); +COMMENT ON TYPE cat_tools.routine_provolatile IS $$Valid values for `pg_proc.provolatile`$$; + +CREATE TYPE cat_tools.routine_volatility AS ENUM( + 'immutable' + , 'stable' + , 'volatile' +); +COMMENT ON TYPE cat_tools.routine_volatility IS $$Volatility levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_proparallel AS ENUM( + 's' -- safe + , 'r' -- restricted + , 'u' -- unsafe +); +COMMENT ON TYPE cat_tools.routine_proparallel IS $$Valid values for `pg_proc.proparallel`$$; + +CREATE TYPE cat_tools.routine_parallel_safety AS ENUM( + 'safe' + , 'restricted' + , 'unsafe' +); +COMMENT ON TYPE cat_tools.routine_parallel_safety IS $$Parallel safety levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_argument AS ( + argument_name text + , argument_type pg_catalog.regtype + , argument_mode cat_tools.routine_argument_mode + , argument_default text +); +COMMENT ON TYPE cat_tools.routine_argument IS $$Detailed information about a single function/procedure argument$$; + + +-- Mapping functions +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind cat_tools.relation_relkind' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* + * The c/f/m arms were previously mapped backwards (c->materialized view, + * f->composite type, m->foreign table), disagreeing with pg_class.relkind + * semantics documented in src/include/catalog/pg_class.h. Correct mapping is + * 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' + WHEN 'p' THEN 'partitioned table' + WHEN 'I' THEN 'partitioned index' +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$ +/* + * The composite type / foreign table / materialized view arms were previously + * mapped backwards (composite type->f, foreign table->m, materialized view->c), + * disagreeing with pg_class.relkind semantics documented in + * src/include/catalog/pg_class.h. Correct mapping is composite type=c, + * foreign table=f, materialized view=m. + */ +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' + WHEN 'partitioned table' THEN 'p' + WHEN 'partitioned index' THEN 'I' +END::cat_tools.relation_relkind +$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +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' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__type' + , 'prokind cat_tools.routine_prokind' + , 'cat_tools.routine_type LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE prokind + WHEN 'f' THEN 'function' + WHEN 'p' THEN 'procedure' + WHEN 'a' THEN 'aggregate' + WHEN 'w' THEN 'window' +END::cat_tools.routine_type +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_prokind to cat_tools.routine_type' +); + +CREATE CAST ("char" AS cat_tools.routine_prokind) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proargmode) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_provolatile) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proparallel) WITH INOUT AS IMPLICIT; + +SELECT __cat_tools.create_function( + 'cat_tools.routine__argument_mode' + , 'proargmode cat_tools.routine_proargmode' + , 'cat_tools.routine_argument_mode LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proargmode + WHEN 'i' THEN 'in' + WHEN 'o' THEN 'out' + WHEN 'b' THEN 'inout' + WHEN 'v' THEN 'variadic' + WHEN 't' THEN 'table' +END::cat_tools.routine_argument_mode +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proargmode to cat_tools.routine_argument_mode' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__volatility' + , 'provolatile cat_tools.routine_provolatile' + , 'cat_tools.routine_volatility LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE provolatile + WHEN 'i' THEN 'immutable' + WHEN 's' THEN 'stable' + WHEN 'v' THEN 'volatile' +END::cat_tools.routine_volatility +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_provolatile to cat_tools.routine_volatility' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parallel_safety' + , 'proparallel cat_tools.routine_proparallel' + , 'cat_tools.routine_parallel_safety LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proparallel + WHEN 's' THEN 'safe' + WHEN 'r' THEN 'restricted' + WHEN 'u' THEN 'unsafe' +END::cat_tools.routine_parallel_safety +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proparallel to cat_tools.routine_parallel_safety' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types' + , $$func pg_catalog.regprocedure$$ + , $$pg_catalog.regtype[] LANGUAGE sql STABLE$$ + , $body$ +SELECT string_to_array(proargtypes::text,' ')::pg_catalog.regtype[] +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as an array of regtype' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names' + , $$func pg_catalog.regprocedure$$ + , $$text[] LANGUAGE sql STABLE$$ + , $body$ +SELECT + CASE + WHEN proargnames IS NULL THEN + -- No named arguments, return array of NULLs matching proargtypes length + CASE + WHEN pronargs > 0 THEN + array_fill(NULL::text, ARRAY[pronargs]) + ELSE + '{}'::text[] + END + WHEN proargmodes IS NULL THEN + -- All arguments are IN mode, proargnames and proargtypes align + array( + SELECT CASE WHEN name = '' THEN NULL ELSE name END + FROM unnest(proargnames) AS name + ) + ELSE + -- Mixed argument modes, need to filter names to match proargtypes + array( + SELECT + CASE + WHEN i <= array_length(proargnames, 1) AND proargnames[i] != '' THEN proargnames[i] + ELSE NULL + END + FROM unnest(proargmodes) WITH ORDINALITY AS t(mode, i) + WHERE mode IN ('i', 'b', 'v') + ) + END +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as an array of text. Empty strings are converted to NULL.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_types($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as a comma-separated text string' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as a comma-separated text string' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_types', 'cat_tools.routine__parse_arg_types'); + result pg_catalog.regtype[]; +BEGIN + result := cat_tools.routine__arg_types(c_temp_proc); + + -- Clean up the temporary function + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_types'); + + RETURN result; +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.routine__parse_arg_names' + , $$arguments text$$ + , $$text[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_names', 'cat_tools.routine__parse_arg_names'); + result text[]; +BEGIN + result := cat_tools.routine__arg_names(c_temp_proc); + + -- Clean up the temporary function + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_names'); + + RETURN result; +END +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as an array. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types + behavior). Unnamed arguments appear as NULL in the result array.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.routine__parse_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.routine__parse_arg_names_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.routine__parse_arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types_text + behavior). Unnamed arguments appear as empty strings in the result.' + +); + +@generated@ + +-- Deprecated wrapper functions for backwards compatibility +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types() is deprecated, use routine__parse_arg_types instead'; + + RETURN cat_tools.routine__parse_arg_types(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types instead. + Returns argument types for a function argument body as regtype[]. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead'; + + RETURN cat_tools.routine__parse_arg_types_text(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types_text instead. + Returns argument types for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +@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.routine__parse_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@ + + +@generated@ + +CREATE TYPE cat_tools.object_type AS ENUM( + -- pg_class + 'table' + , 'index' + , 'sequence' + , 'toast table' + , 'view' + , 'materialized view' + , 'composite type' + , 'foreign table' + , 'partitioned table' + , 'partitioned index' + /* + * 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' + , 'partitioned table' + , 'partitioned index' + ]::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@ + +@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 unnest($2) WITH ORDINALITY AS t(attnum, i) + JOIN pg_catalog.pg_attribute a ON a.attnum = t.attnum + 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.relation__is_temp' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text ~ '^pg_temp' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , $$Returns true if the relation is a temporary table (lives in a schema that starts with 'pg_temp').$$ +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__is_catalog' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text = 'pg_catalog' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , 'Returns true if the relation is in the pg_catalog schema.' +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__column_names' + , 'relation pg_catalog.regclass' + , $$text[] LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT array_agg(quote_ident(attname) ORDER BY attnum) +FROM pg_catalog.pg_attribute +WHERE attrelid = $1 + AND attnum > 0 + AND NOT attisdropped +$body$ + , 'cat_tools__usage' + , 'Returns an array of quoted column names for a relation in ordinal position order.' +); + +@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/sql/cat_tools.sql.in b/sql/cat_tools.sql.in index 6cdbdca..23ed69c 100644 --- a/sql/cat_tools.sql.in +++ b/sql/cat_tools.sql.in @@ -1,7 +1,5 @@ @generated@ -SET LOCAL client_min_messages = WARNING; - DO $$ BEGIN CREATE ROLE cat_tools__usage NOLOGIN; @@ -76,7 +74,7 @@ REVOKE ALL ON _cat_tools.pg_class_v FROM public; * function that we're about to create to create the real version of this * function. */ -CREATE FUNCTION cat_tools.function__arg_types_text(text +CREATE FUNCTION cat_tools.routine__parse_arg_types_text(text ) RETURNS text LANGUAGE sql AS 'SELECT $1'; CREATE FUNCTION __cat_tools.create_function( @@ -88,7 +86,7 @@ CREATE FUNCTION __cat_tools.create_function( , comment text DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $body$ DECLARE - c_simple_args CONSTANT text := cat_tools.function__arg_types_text(args); + c_simple_args CONSTANT text := cat_tools.routine__parse_arg_types_text(args); create_template CONSTANT text := $template$ CREATE OR REPLACE FUNCTION %s( @@ -161,24 +159,49 @@ $body$; @generated@ +/* + * These _cat_tools helper functions are created via __cat_tools.create_function + * so that the REVOKE-from-PUBLIC + GRANT-to-cat_tools__usage is applied + * uniformly and cannot drift from the update script (a hand-written REVOKE was + * previously forgotten there). At this point cat_tools.routine__parse_arg_types_text + * is still the pass-through stub, so create_function's generated REVOKE/GRANT + * carry the argument names rather than bare types; that is harmless because + * PostgreSQL ignores argument names when matching a function signature. + */ SELECT __cat_tools.create_function( - 'cat_tools.function__arg_types' - , $$arguments text$$ - , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + '_cat_tools.function__arg_to_regprocedure' + , 'arguments text, function_suffix text, api_function_name text' + , 'pg_catalog.regprocedure 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( + /* + * Template for creating a temporary function with the user-provided argument + * signature. This allows us to leverage PostgreSQL's parser to validate and + * extract argument information without permanently creating a function. + * Using plpgsql language for the temp function to handle any return type. + */ + c_template CONSTANT text := $fmt$CREATE FUNCTION pg_temp.cat_tools__function__%s__temp_function( %s - ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN NULL; END' + ) RETURNS %s LANGUAGE plpgsql AS 'BEGIN RETURN; END' $fmt$; temp_proc pg_catalog.regprocedure; sql text; BEGIN + /* + * Security check: Ensure current_user == session_user to detect SECURITY DEFINER context + * This prevents SQL injection attacks through elevated privileges. + */ + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' /* invalid_authorization_specification */ + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('current_user is %s, session_user is %s', current_user, session_user) + , HINT = 'Helper functions must not be called from SECURITY DEFINER context.'; + END IF; sql := format( c_template + , function_suffix , arguments , 'void' ); @@ -191,6 +214,7 @@ BEGIN v_type := (regexp_matches( SQLERRM, 'function result type must be ([^ ]+) because of' ))[1]; sql := format( c_template + , function_suffix , arguments , v_type ); @@ -203,20 +227,399 @@ BEGIN * 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 - ); + $$SELECT 'pg_temp.cat_tools__function__%s__temp_function'::pg_catalog.regproc::pg_catalog.regprocedure$$ + , function_suffix + ) INTO temp_proc; + + RETURN temp_proc; +END +$body$ + , 'cat_tools__usage' +); + +SELECT __cat_tools.create_function( + '_cat_tools.function__drop_temp' + , 'p_regprocedure pg_catalog.regprocedure, api_function_name text' + , 'void LANGUAGE plpgsql' + , $body$ +BEGIN + /* + * Security check: Ensure current_user == session_user to detect SECURITY DEFINER context + * This prevents SQL injection attacks through elevated privileges. + */ + IF current_user != session_user THEN + RAISE EXCEPTION USING + ERRCODE = '28000' /* invalid_authorization_specification */ + , MESSAGE = 'potential use of SECURITY DEFINER detected' + , DETAIL = format('API function %s must not be called from a SECURITY DEFINER function', api_function_name) + , HINT = 'We detect SECURITY DEFINER context by comparing current_user and session_user, which can cause false positives if SET ROLE is used'; + END IF; + + EXECUTE 'DROP ROUTINE ' || p_regprocedure; +END +$body$ + , 'cat_tools__usage' +); + +GRANT USAGE ON SCHEMA _cat_tools TO cat_tools__usage; + +@generated@ + +-- Data type definitions +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' + , 'partitioned table' + , 'partitioned index' +); +COMMENT ON TYPE cat_tools.relation_type IS $$Types of objects stored in `pg_class`$$; + +CREATE TYPE cat_tools.relation_relkind AS ENUM( + 'r' -- table + , 'i' -- index + , 'S' -- sequence + , 't' -- toast table + , 'v' -- view + , 'c' -- composite type + , 'f' -- foreign table + , 'm' -- materialized view + , 'p' -- partitioned table + , 'I' -- partitioned index +); +COMMENT ON TYPE cat_tools.relation_relkind IS $$Valid values for `pg_class.relkind`$$; + +CREATE TYPE cat_tools.routine_prokind AS ENUM( + 'f' -- function + , 'p' -- procedure + , 'a' -- aggregate + , 'w' -- window +); +COMMENT ON TYPE cat_tools.routine_prokind IS $$Valid values for `pg_proc.prokind`$$; + +CREATE TYPE cat_tools.routine_type AS ENUM( + 'function' + , 'procedure' + , 'aggregate' + , 'window' +); +COMMENT ON TYPE cat_tools.routine_type IS $$Types of routines stored in `pg_proc`$$; + +CREATE TYPE cat_tools.routine_proargmode AS ENUM( + 'i' -- in + , 'o' -- out + , 'b' -- inout + , 'v' -- variadic + , 't' -- table +); +COMMENT ON TYPE cat_tools.routine_proargmode IS $$Valid values for `pg_proc.proargmodes` elements$$; + +CREATE TYPE cat_tools.routine_argument_mode AS ENUM( + 'in' + , 'out' + , 'inout' + , 'variadic' + , 'table' +); +COMMENT ON TYPE cat_tools.routine_argument_mode IS $$Argument modes for function/procedure parameters$$; + +CREATE TYPE cat_tools.routine_provolatile AS ENUM( + 'i' -- immutable + , 's' -- stable + , 'v' -- volatile +); +COMMENT ON TYPE cat_tools.routine_provolatile IS $$Valid values for `pg_proc.provolatile`$$; + +CREATE TYPE cat_tools.routine_volatility AS ENUM( + 'immutable' + , 'stable' + , 'volatile' +); +COMMENT ON TYPE cat_tools.routine_volatility IS $$Volatility levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_proparallel AS ENUM( + 's' -- safe + , 'r' -- restricted + , 'u' -- unsafe +); +COMMENT ON TYPE cat_tools.routine_proparallel IS $$Valid values for `pg_proc.proparallel`$$; + +CREATE TYPE cat_tools.routine_parallel_safety AS ENUM( + 'safe' + , 'restricted' + , 'unsafe' +); +COMMENT ON TYPE cat_tools.routine_parallel_safety IS $$Parallel safety levels for functions/procedures$$; + +CREATE TYPE cat_tools.routine_argument AS ( + argument_name text + , argument_type pg_catalog.regtype + , argument_mode cat_tools.routine_argument_mode + , argument_default text +); +COMMENT ON TYPE cat_tools.routine_argument IS $$Detailed information about a single function/procedure argument$$; + + +-- Mapping functions +SELECT __cat_tools.create_function( + 'cat_tools.relation__kind' + , 'relkind cat_tools.relation_relkind' + , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' + , $body$ +/* + * The c/f/m arms were previously mapped backwards (c->materialized view, + * f->composite type, m->foreign table), disagreeing with pg_class.relkind + * semantics documented in src/include/catalog/pg_class.h. Correct mapping is + * 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' + WHEN 'p' THEN 'partitioned table' + WHEN 'I' THEN 'partitioned index' +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$ +/* + * The composite type / foreign table / materialized view arms were previously + * mapped backwards (composite type->f, foreign table->m, materialized view->c), + * disagreeing with pg_class.relkind semantics documented in + * src/include/catalog/pg_class.h. Correct mapping is composite type=c, + * foreign table=f, materialized view=m. + */ +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' + WHEN 'partitioned table' THEN 'p' + WHEN 'partitioned index' THEN 'I' +END::cat_tools.relation_relkind +$body$ + , 'cat_tools__usage' + , 'Mapping from to a value' +); + +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' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__type' + , 'prokind cat_tools.routine_prokind' + , 'cat_tools.routine_type LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE prokind + WHEN 'f' THEN 'function' + WHEN 'p' THEN 'procedure' + WHEN 'a' THEN 'aggregate' + WHEN 'w' THEN 'window' +END::cat_tools.routine_type +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_prokind to cat_tools.routine_type' +); + +CREATE CAST ("char" AS cat_tools.routine_prokind) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proargmode) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_provolatile) WITH INOUT AS IMPLICIT; +CREATE CAST ("char" AS cat_tools.routine_proparallel) WITH INOUT AS IMPLICIT; + +SELECT __cat_tools.create_function( + 'cat_tools.routine__argument_mode' + , 'proargmode cat_tools.routine_proargmode' + , 'cat_tools.routine_argument_mode LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proargmode + WHEN 'i' THEN 'in' + WHEN 'o' THEN 'out' + WHEN 'b' THEN 'inout' + WHEN 'v' THEN 'variadic' + WHEN 't' THEN 'table' +END::cat_tools.routine_argument_mode +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proargmode to cat_tools.routine_argument_mode' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__volatility' + , 'provolatile cat_tools.routine_provolatile' + , 'cat_tools.routine_volatility LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE provolatile + WHEN 'i' THEN 'immutable' + WHEN 's' THEN 'stable' + WHEN 'v' THEN 'volatile' +END::cat_tools.routine_volatility +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_provolatile to cat_tools.routine_volatility' +); + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parallel_safety' + , 'proparallel cat_tools.routine_proparallel' + , 'cat_tools.routine_parallel_safety LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE' + , $body$ +SELECT CASE proparallel + WHEN 's' THEN 'safe' + WHEN 'r' THEN 'restricted' + WHEN 'u' THEN 'unsafe' +END::cat_tools.routine_parallel_safety +$body$ + , 'cat_tools__usage' + , 'Mapping from cat_tools.routine_proparallel to cat_tools.routine_parallel_safety' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types' + , $$func pg_catalog.regprocedure$$ + , $$pg_catalog.regtype[] LANGUAGE sql STABLE$$ + , $body$ +SELECT string_to_array(proargtypes::text,' ')::pg_catalog.regtype[] +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as an array of regtype' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names' + , $$func pg_catalog.regprocedure$$ + , $$text[] LANGUAGE sql STABLE$$ + , $body$ +SELECT + CASE + WHEN proargnames IS NULL THEN + -- No named arguments, return array of NULLs matching proargtypes length + CASE + WHEN pronargs > 0 THEN + array_fill(NULL::text, ARRAY[pronargs]) + ELSE + '{}'::text[] + END + WHEN proargmodes IS NULL THEN + -- All arguments are IN mode, proargnames and proargtypes align + array( + SELECT CASE WHEN name = '' THEN NULL ELSE name END + FROM unnest(proargnames) AS name + ) + ELSE + -- Mixed argument modes, need to filter names to match proargtypes + array( + SELECT + CASE + WHEN i <= array_length(proargnames, 1) AND proargnames[i] != '' THEN proargnames[i] + ELSE NULL + END + FROM unnest(proargmodes) WITH ORDINALITY AS t(mode, i) + WHERE mode IN ('i', 'b', 'v') + ) + END +FROM pg_proc +WHERE oid = $1::pg_catalog.regproc +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as an array of text. Empty strings are converted to NULL.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_types_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_types($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument types for a function as a comma-separated text string' +); - RETURN input_arg_types; +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__arg_names_text' + , $$func pg_catalog.regprocedure$$ + , $$text LANGUAGE sql STABLE$$ + , $body$ +SELECT array_to_string(cat_tools.routine__arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns all argument names for a function as a comma-separated text string' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_types', 'cat_tools.routine__parse_arg_types'); + result pg_catalog.regtype[]; +BEGIN + result := cat_tools.routine__arg_types(c_temp_proc); + + -- Clean up the temporary function + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_types'); + + RETURN result; END $body$ , 'cat_tools__usage' @@ -228,11 +631,36 @@ $body$ @generated@ SELECT __cat_tools.create_function( - 'cat_tools.function__arg_types_text' + 'cat_tools.routine__parse_arg_names' + , $$arguments text$$ + , $$text[] LANGUAGE plpgsql$$ + , $body$ +DECLARE + c_temp_proc CONSTANT pg_catalog.regprocedure := _cat_tools.function__arg_to_regprocedure(arguments, 'arg_names', 'cat_tools.routine__parse_arg_names'); + result text[]; +BEGIN + result := cat_tools.routine__arg_names(c_temp_proc); + + -- Clean up the temporary function + PERFORM _cat_tools.function__drop_temp(c_temp_proc, 'cat_tools.routine__parse_arg_names'); + + RETURN result; +END +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as an array. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types + behavior). Unnamed arguments appear as NULL in the result array.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_types_text' , $$arguments text$$ , $$text LANGUAGE sql$$ , $body$ -SELECT array_to_string(cat_tools.function__arg_types($1), ', ') +SELECT array_to_string(cat_tools.routine__parse_arg_types($1), ', ') $body$ , 'cat_tools__usage' , 'Returns argument types for a function argument body as text. Unlike a @@ -243,6 +671,61 @@ $body$ @generated@ +SELECT __cat_tools.create_function( + 'cat_tools.routine__parse_arg_names_text' + , $$arguments text$$ + , $$text LANGUAGE sql$$ + , $body$ +SELECT array_to_string(cat_tools.routine__parse_arg_names($1), ', ') +$body$ + , 'cat_tools__usage' + , 'Returns argument names for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments (matching routine__parse_arg_types_text + behavior). Unnamed arguments appear as empty strings in the result.' + +); + +@generated@ + +-- Deprecated wrapper functions for backwards compatibility +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types' + , $$arguments text$$ + , $$pg_catalog.regtype[] LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types() is deprecated, use routine__parse_arg_types instead'; + + RETURN cat_tools.routine__parse_arg_types(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types instead. + Returns argument types for a function argument body as regtype[]. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +@generated@ + +SELECT __cat_tools.create_function( + 'cat_tools.function__arg_types_text' + , $$arguments text$$ + , $$text LANGUAGE plpgsql$$ + , $body$ +BEGIN + RAISE WARNING 'function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead'; + + RETURN cat_tools.routine__parse_arg_types_text(arguments); +END +$body$ + , 'cat_tools__usage' + , 'DEPRECATED: Use routine__parse_arg_types_text instead. + Returns argument types for a function argument body as text. Only + includes IN, INOUT, and VARIADIC arguments.' +); + +@generated@ + SELECT __cat_tools.create_function( 'cat_tools.regprocedure' , $$ @@ -253,7 +736,7 @@ SELECT __cat_tools.create_function( SELECT format( '%s(%s)' , $1 - , cat_tools.function__arg_types_text($2) + , cat_tools.routine__parse_arg_types_text($2) )::pg_catalog.regprocedure $body$ , 'cat_tools__usage' @@ -265,38 +748,6 @@ $body$ @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@ @@ -310,6 +761,8 @@ CREATE TYPE cat_tools.object_type AS ENUM( , 'materialized view' , 'composite type' , 'foreign table' + , 'partitioned table' + , 'partitioned index' /* * 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! @@ -361,6 +814,7 @@ CREATE TYPE cat_tools.object_type AS ENUM( , 'access method' -- pg_am ); + @generated@ SELECT __cat_tools.create_function( @@ -471,6 +925,8 @@ SELECT ( , 'materialized view' , 'composite type' , 'foreign table' + , 'partitioned table' + , 'partitioned index' ]::cat_tools.object_type[] ) THEN 'pg_class' WHEN object_type = ANY( '{domain constraint,table constraint}'::cat_tools.object_type[] ) @@ -641,65 +1097,6 @@ $body$ @generated@ -SELECT __cat_tools.create_function( - 'cat_tools.relation__kind' - , 'relkind cat_tools.relation_relkind' - , 'cat_tools.relation_type LANGUAGE sql STRICT IMMUTABLE' - , $body$ -SELECT CASE relkind - WHEN 'r' THEN 'table' - WHEN 'i' THEN 'index' - WHEN 'S' THEN 'sequence' - WHEN 't' THEN 'toast table' - WHEN 'v' THEN 'view' - WHEN 'c' THEN 'materialized view' - WHEN 'f' THEN 'composite type' - WHEN 'm' THEN 'foreign table' -END::cat_tools.relation_type -$body$ - , 'cat_tools__usage' - , 'Mapping from to a ' -); - -SELECT __cat_tools.create_function( - 'cat_tools.relation__relkind' - , 'kind cat_tools.relation_type' - , 'cat_tools.relation_relkind LANGUAGE sql STRICT IMMUTABLE' - , $body$ -SELECT CASE kind - WHEN 'table' THEN 'r' - WHEN 'index' THEN 'i' - WHEN 'sequence' THEN 'S' - WHEN 'toast table' THEN 't' - WHEN 'view' THEN 'v' - WHEN 'materialized view' THEN 'c' - WHEN 'composite type' THEN 'f' - WHEN 'foreign table' THEN 'm' -END::cat_tools.relation_relkind -$body$ - , 'cat_tools__usage' - , 'Mapping from to a value' -); - -@generated@ - -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! @@ -854,8 +1251,8 @@ SELECT __cat_tools.create_function( , $$ 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] + FROM unnest($2) WITH ORDINALITY AS t(attnum, i) + JOIN pg_catalog.pg_attribute a ON a.attnum = t.attnum WHERE attrelid = $1 ORDER BY i ) @@ -1228,6 +1625,49 @@ $body$ @generated@ +SELECT __cat_tools.create_function( + 'cat_tools.relation__is_temp' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text ~ '^pg_temp' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , $$Returns true if the relation is a temporary table (lives in a schema that starts with 'pg_temp').$$ +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__is_catalog' + , 'relation pg_catalog.regclass' + , $$boolean LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT relnamespace::pg_catalog.regnamespace::text = 'pg_catalog' +FROM pg_catalog.pg_class +WHERE oid = $1 +$body$ + , 'cat_tools__usage' + , 'Returns true if the relation is in the pg_catalog schema.' +); + +SELECT __cat_tools.create_function( + 'cat_tools.relation__column_names' + , 'relation pg_catalog.regclass' + , $$text[] LANGUAGE sql STRICT STABLE$$ + , $body$ +SELECT array_agg(quote_ident(attname) ORDER BY attnum) +FROM pg_catalog.pg_attribute +WHERE attrelid = $1 + AND attnum > 0 + AND NOT attisdropped +$body$ + , 'cat_tools__usage' + , 'Returns an array of quoted column names for a relation in ordinal position order.' +); + +@generated@ + SELECT __cat_tools.create_function( 'cat_tools.name__check' , 'name_to_check text' diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..8fb1f0a --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,2 @@ +# Generated per-PG-version by test/gen-relkinds.sh (see Makefile) +/.generated/ diff --git a/test/build/build.sql b/test/build/build.sql new file mode 100644 index 0000000..ef6210a --- /dev/null +++ b/test/build/build.sql @@ -0,0 +1,21 @@ +\set ECHO none + +\i test/pgxntool/psql.sql +\t + +BEGIN; +CREATE SCHEMA cat_tools; + +/* + * The install script no longer sets client_min_messages itself (that is the + * caller's job; deps.sql does the same for the main suite). Suppress NOTICEs + * here so build.out does not capture verbose, version-specific messages (e.g. + * "%TYPE converted to regclass" with a source-file LOCATION line). + */ +SET client_min_messages = WARNING; + +\i sql/cat_tools.sql + +\echo # TRANSACTION INTENTIONALLY LEFT OPEN! + +-- vi: expandtab sw=2 ts=2 diff --git a/test/expected/zzz_build.out b/test/build/expected/build.out similarity index 79% rename from test/expected/zzz_build.out rename to test/build/expected/build.out index dbe9615..abc561f 100644 --- a/test/expected/zzz_build.out +++ b/test/build/expected/build.out @@ -82,6 +82,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..2fcb3ac --- /dev/null +++ b/test/build/upgrade.sql @@ -0,0 +1,28 @@ +\set ECHO none +\i test/pgxntool/psql.sql +\t + +/* + * Sanity check: install a previous version and update to current. + * + * The 0.2.3β†’0.3.0 update script uses ALTER TYPE ... ADD VALUE, which cannot + * run inside a transaction block or in an extension update script + * (PROCESS_UTILITY_QUERY context) on PG11 and below. This restriction was + * lifted in PG12. PG11 and below are therefore skipped entirely. + */ +SELECT current_setting('server_version_num')::int >= 120000 AS pg12plus \gset + +\if :pg12plus +BEGIN; +CREATE EXTENSION cat_tools VERSION '0.2.2'; +-- Suppress expected deprecation warnings from the update. +SET LOCAL client_min_messages = ERROR; +ALTER EXTENSION cat_tools UPDATE; +ROLLBACK; +\else +/* + * PG11 and below: skip the update test. ALTER TYPE ... ADD VALUE cannot run + * inside a transaction block or in an extension update script on PG11 and + * below (PROCESS_UTILITY_QUERY context). This restriction was lifted in PG12. + */ +\endif diff --git a/test/deps.sql b/test/deps.sql index f7fe4d9..6e21ea3 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -1,20 +1,16 @@ -- IF NOT EXISTS will emit NOTICEs, which is annoying SET client_min_messages = WARNING; --- Add any test dependency statements here --- Note: pgTap is loaded by setup.sql ---CREATE EXTENSION IF NOT EXISTS ...; - -\i test/.build/active.sql - --- Used by several unit tests -\set no_use_role cat_tools_testing__no_use_role -\set use_role cat_tools_testing__use_role -CREATE ROLE :no_use_role; -CREATE ROLE :use_role; - -GRANT cat_tools__usage TO :use_role; --- PG15+ removed CREATE on public schema from PUBLIC; grant it explicitly for tests --- that need to create shadow names in public to test catalog lookup correctness. -GRANT CREATE ON SCHEMA public TO :use_role; - +/* + * The extension and the test roles are installed ONCE, COMMITTED, before the + * suite by test/install/load.sql (pgxntool's test/install feature), instead of + * per-test here. So this file no longer runs CREATE EXTENSION or CREATE ROLE: + * all it does is (re)set the psql variables the suite references (setup.sql and + * the test/sql/ files). psql variables are session-local, not committed DB + * state, so they must still be (re)set per test file. + * + * The role names live in exactly one place, test/roles.sql, which + * test/install/load.sql also loads. Add any OTHER per-test dependency + * \set/statements here; committed dependencies belong in test/install/load.sql. + */ +\i test/roles.sql diff --git a/test/expected/function.out b/test/expected/function.out index 29dade1..8be118c 100644 --- a/test/expected/function.out +++ b/test/expected/function.out @@ -1,11 +1,14 @@ \set ECHO none -1..8 -ok 1 - Verify public has no perms -ok 2 - Verify public has no perms -ok 3 - Verify function__arg_types() with INOUT and OUT -ok 4 - Verify function__arg_types() with just INOUT -ok 5 - Verify function__arg_types() with just OUT -ok 6 - Verify function__arg_types() with only inputs -ok 7 - Create pg_temp.test_function(anyarray, OUT text, OUT "char", pg_class, int, VARIADIC boolean[]) -ok 8 - Verify regprocedure() +1..7 +ok 1 - Function _cat_tools.function__arg_to_regprocedure(text, text, text) should not be security definer +ok 2 - Function _cat_tools.function__drop_temp(regprocedure, text) should not be security definer +ok 3 - Verify regprocedure() +WARNING: function__arg_types() is deprecated, use routine__parse_arg_types instead +ok 4 - Verify deprecated function__arg_types() with INOUT and OUT +WARNING: function__arg_types() is deprecated, use routine__parse_arg_types instead +ok 5 - Verify deprecated function__arg_types() with simple args +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +ok 6 - Verify deprecated function__arg_types_text() with INOUT and OUT +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +ok 7 - Verify deprecated function__arg_types_text() with simple args # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/object_type.out b/test/expected/object_type.out index ea37620..1c9029b 100644 --- a/test/expected/object_type.out +++ b/test/expected/object_type.out @@ -1,5 +1,5 @@ \set ECHO none -1..222 +1..230 ok 1 - sanity check size of pg_temp.extra_types() ok 2 - sanity check size of pg_temp.obj_type ok 3 - Permission denied trying to use types @@ -38,188 +38,196 @@ ok 35 - check addressability for object type 'operator' ok 36 - check addressability for object type 'operator class' ok 37 - check addressability for object type 'operator family' ok 38 - check addressability for object type 'operator of access method' -ok 39 - check addressability for object type 'policy' -ok 40 - check addressability for object type 'role' -ok 41 - check addressability for object type 'rule' -ok 42 - check addressability for object type 'schema' -ok 43 - check addressability for object type 'sequence' -ok 44 - check addressability for object type 'sequence column' -ok 45 - check addressability for object type 'server' -ok 46 - check addressability for object type 'table' -ok 47 - check addressability for object type 'table column' -ok 48 - check addressability for object type 'table constraint' -ok 49 - check addressability for object type 'tablespace' -ok 50 - check addressability for object type 'text search configuration' -ok 51 - check addressability for object type 'text search dictionary' -ok 52 - check addressability for object type 'text search parser' -ok 53 - check addressability for object type 'text search template' -ok 54 - check addressability for object type 'toast table' -ok 55 - check addressability for object type 'toast table column' -ok 56 - check addressability for object type 'transform' -ok 57 - check addressability for object type 'trigger' -ok 58 - check addressability for object type 'type' -ok 59 - check addressability for object type 'user mapping' -ok 60 - check addressability for object type 'view' -ok 61 - check addressability for object type 'view column' -ok 62 - lives_ok: SELECT * FROM cat_tools.object__catalog('access method') -ok 63 - lives_ok: SELECT * FROM cat_tools.object__catalog('aggregate') -ok 64 - lives_ok: SELECT * FROM cat_tools.object__catalog('cast') -ok 65 - lives_ok: SELECT * FROM cat_tools.object__catalog('collation') -ok 66 - lives_ok: SELECT * FROM cat_tools.object__catalog('composite type') -ok 67 - lives_ok: SELECT * FROM cat_tools.object__catalog('composite type column') -ok 68 - lives_ok: SELECT * FROM cat_tools.object__catalog('conversion') -ok 69 - lives_ok: SELECT * FROM cat_tools.object__catalog('database') -ok 70 - lives_ok: SELECT * FROM cat_tools.object__catalog('default acl') -ok 71 - lives_ok: SELECT * FROM cat_tools.object__catalog('default value') -ok 72 - lives_ok: SELECT * FROM cat_tools.object__catalog('domain constraint') -ok 73 - lives_ok: SELECT * FROM cat_tools.object__catalog('event trigger') -ok 74 - lives_ok: SELECT * FROM cat_tools.object__catalog('extension') -ok 75 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign table') -ok 76 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign table column') -ok 77 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign-data wrapper') -ok 78 - lives_ok: SELECT * FROM cat_tools.object__catalog('function') -ok 79 - lives_ok: SELECT * FROM cat_tools.object__catalog('function of access method') -ok 80 - lives_ok: SELECT * FROM cat_tools.object__catalog('index') -ok 81 - lives_ok: SELECT * FROM cat_tools.object__catalog('index column') -ok 82 - lives_ok: SELECT * FROM cat_tools.object__catalog('language') -ok 83 - lives_ok: SELECT * FROM cat_tools.object__catalog('large object') -ok 84 - lives_ok: SELECT * FROM cat_tools.object__catalog('materialized view') -ok 85 - lives_ok: SELECT * FROM cat_tools.object__catalog('materialized view column') -ok 86 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator') -ok 87 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator class') -ok 88 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator family') -ok 89 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator of access method') -ok 90 - lives_ok: SELECT * FROM cat_tools.object__catalog('policy') -ok 91 - lives_ok: SELECT * FROM cat_tools.object__catalog('role') -ok 92 - lives_ok: SELECT * FROM cat_tools.object__catalog('rule') -ok 93 - lives_ok: SELECT * FROM cat_tools.object__catalog('schema') -ok 94 - lives_ok: SELECT * FROM cat_tools.object__catalog('sequence') -ok 95 - lives_ok: SELECT * FROM cat_tools.object__catalog('sequence column') -ok 96 - lives_ok: SELECT * FROM cat_tools.object__catalog('server') -ok 97 - lives_ok: SELECT * FROM cat_tools.object__catalog('table') -ok 98 - lives_ok: SELECT * FROM cat_tools.object__catalog('table column') -ok 99 - lives_ok: SELECT * FROM cat_tools.object__catalog('table constraint') -ok 100 - lives_ok: SELECT * FROM cat_tools.object__catalog('tablespace') -ok 101 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search configuration') -ok 102 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search dictionary') -ok 103 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search parser') -ok 104 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search template') -ok 105 - lives_ok: SELECT * FROM cat_tools.object__catalog('toast table') -ok 106 - lives_ok: SELECT * FROM cat_tools.object__catalog('toast table column') -ok 107 - lives_ok: SELECT * FROM cat_tools.object__catalog('transform') -ok 108 - lives_ok: SELECT * FROM cat_tools.object__catalog('trigger') -ok 109 - lives_ok: SELECT * FROM cat_tools.object__catalog('type') -ok 110 - lives_ok: SELECT * FROM cat_tools.object__catalog('user mapping') -ok 111 - lives_ok: SELECT * FROM cat_tools.object__catalog('view') -ok 112 - lives_ok: SELECT * FROM cat_tools.object__catalog('view column') -ok 113 - lives_ok: SELECT * FROM cat_tools.object__reg_type('access method') -ok 114 - lives_ok: SELECT * FROM cat_tools.object__reg_type('aggregate') -ok 115 - lives_ok: SELECT * FROM cat_tools.object__reg_type('cast') -ok 116 - lives_ok: SELECT * FROM cat_tools.object__reg_type('collation') -ok 117 - lives_ok: SELECT * FROM cat_tools.object__reg_type('composite type') -ok 118 - lives_ok: SELECT * FROM cat_tools.object__reg_type('composite type column') -ok 119 - lives_ok: SELECT * FROM cat_tools.object__reg_type('conversion') -ok 120 - lives_ok: SELECT * FROM cat_tools.object__reg_type('database') -ok 121 - lives_ok: SELECT * FROM cat_tools.object__reg_type('default acl') -ok 122 - lives_ok: SELECT * FROM cat_tools.object__reg_type('default value') -ok 123 - lives_ok: SELECT * FROM cat_tools.object__reg_type('domain constraint') -ok 124 - lives_ok: SELECT * FROM cat_tools.object__reg_type('event trigger') -ok 125 - lives_ok: SELECT * FROM cat_tools.object__reg_type('extension') -ok 126 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign table') -ok 127 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign table column') -ok 128 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign-data wrapper') -ok 129 - lives_ok: SELECT * FROM cat_tools.object__reg_type('function') -ok 130 - lives_ok: SELECT * FROM cat_tools.object__reg_type('function of access method') -ok 131 - lives_ok: SELECT * FROM cat_tools.object__reg_type('index') -ok 132 - lives_ok: SELECT * FROM cat_tools.object__reg_type('index column') -ok 133 - lives_ok: SELECT * FROM cat_tools.object__reg_type('language') -ok 134 - lives_ok: SELECT * FROM cat_tools.object__reg_type('large object') -ok 135 - lives_ok: SELECT * FROM cat_tools.object__reg_type('materialized view') -ok 136 - lives_ok: SELECT * FROM cat_tools.object__reg_type('materialized view column') -ok 137 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator') -ok 138 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator class') -ok 139 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator family') -ok 140 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator of access method') -ok 141 - lives_ok: SELECT * FROM cat_tools.object__reg_type('policy') -ok 142 - lives_ok: SELECT * FROM cat_tools.object__reg_type('role') -ok 143 - lives_ok: SELECT * FROM cat_tools.object__reg_type('rule') -ok 144 - lives_ok: SELECT * FROM cat_tools.object__reg_type('schema') -ok 145 - lives_ok: SELECT * FROM cat_tools.object__reg_type('sequence') -ok 146 - lives_ok: SELECT * FROM cat_tools.object__reg_type('sequence column') -ok 147 - lives_ok: SELECT * FROM cat_tools.object__reg_type('server') -ok 148 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table') -ok 149 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table column') -ok 150 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table constraint') -ok 151 - lives_ok: SELECT * FROM cat_tools.object__reg_type('tablespace') -ok 152 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search configuration') -ok 153 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search dictionary') -ok 154 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search parser') -ok 155 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search template') -ok 156 - lives_ok: SELECT * FROM cat_tools.object__reg_type('toast table') -ok 157 - lives_ok: SELECT * FROM cat_tools.object__reg_type('toast table column') -ok 158 - lives_ok: SELECT * FROM cat_tools.object__reg_type('transform') -ok 159 - lives_ok: SELECT * FROM cat_tools.object__reg_type('trigger') -ok 160 - lives_ok: SELECT * FROM cat_tools.object__reg_type('type') -ok 161 - lives_ok: SELECT * FROM cat_tools.object__reg_type('user mapping') -ok 162 - lives_ok: SELECT * FROM cat_tools.object__reg_type('view') -ok 163 - lives_ok: SELECT * FROM cat_tools.object__reg_type('view column') -ok 164 - Verify cat_tools.object__address_classid('access method') -ok 165 - Verify cat_tools.object__address_classid('aggregate') -ok 166 - Verify cat_tools.object__address_classid('cast') -ok 167 - Verify cat_tools.object__address_classid('collation') -ok 168 - Verify cat_tools.object__address_classid('composite type') -ok 169 - Verify cat_tools.object__address_classid('composite type column') -ok 170 - Verify cat_tools.object__address_classid('conversion') -ok 171 - Verify cat_tools.object__address_classid('database') -ok 172 - Verify cat_tools.object__address_classid('default acl') -ok 173 - Verify cat_tools.object__address_classid('default value') -ok 174 - Verify cat_tools.object__address_classid('domain constraint') -ok 175 - Verify cat_tools.object__address_classid('event trigger') -ok 176 - Verify cat_tools.object__address_classid('extension') -ok 177 - Verify cat_tools.object__address_classid('foreign table') -ok 178 - Verify cat_tools.object__address_classid('foreign table column') -ok 179 - Verify cat_tools.object__address_classid('foreign-data wrapper') -ok 180 - Verify cat_tools.object__address_classid('function') -ok 181 - Verify cat_tools.object__address_classid('function of access method') -ok 182 - Verify cat_tools.object__address_classid('index') -ok 183 - Verify cat_tools.object__address_classid('index column') -ok 184 - Verify cat_tools.object__address_classid('language') -ok 185 - Verify cat_tools.object__address_classid('large object') -ok 186 - Verify cat_tools.object__address_classid('materialized view') -ok 187 - Verify cat_tools.object__address_classid('materialized view column') -ok 188 - Verify cat_tools.object__address_classid('operator') -ok 189 - Verify cat_tools.object__address_classid('operator class') -ok 190 - Verify cat_tools.object__address_classid('operator family') -ok 191 - Verify cat_tools.object__address_classid('operator of access method') -ok 192 - Verify cat_tools.object__address_classid('policy') -ok 193 - Verify cat_tools.object__address_classid('role') -ok 194 - Verify cat_tools.object__address_classid('rule') -ok 195 - Verify cat_tools.object__address_classid('schema') -ok 196 - Verify cat_tools.object__address_classid('sequence') -ok 197 - Verify cat_tools.object__address_classid('sequence column') -ok 198 - Verify cat_tools.object__address_classid('server') -ok 199 - Verify cat_tools.object__address_classid('table') -ok 200 - Verify cat_tools.object__address_classid('table column') -ok 201 - Verify cat_tools.object__address_classid('table constraint') -ok 202 - Verify cat_tools.object__address_classid('tablespace') -ok 203 - Verify cat_tools.object__address_classid('text search configuration') -ok 204 - Verify cat_tools.object__address_classid('text search dictionary') -ok 205 - Verify cat_tools.object__address_classid('text search parser') -ok 206 - Verify cat_tools.object__address_classid('text search template') -ok 207 - Verify cat_tools.object__address_classid('toast table') -ok 208 - Verify cat_tools.object__address_classid('toast table column') -ok 209 - Verify cat_tools.object__address_classid('transform') -ok 210 - Verify cat_tools.object__address_classid('trigger') -ok 211 - Verify cat_tools.object__address_classid('type') -ok 212 - Verify cat_tools.object__address_classid('user mapping') -ok 213 - Verify cat_tools.object__address_classid('view') -ok 214 - Verify cat_tools.object__address_classid('view column') -ok 215 - Change search_path -ok 216 - Create bogus pg_class table -ok 217 - Create bogus regclass type -ok 218 - Simple 'pg_class'::pg_catalog.regclass should not return pg_catalog.pg_class -ok 219 - Simple 'regclass'::regtype should not return pg_catalog.regtype -ok 220 - cat_tools.object__catalog('table') returns pg_catalog.pg_class -ok 221 - cat_tools.object__catalog('table') returns pg_catalog.pg_class -ok 222 - Verify objects__shared_src() returns correct values +ok 39 - check addressability for object type 'partitioned index' +ok 40 - check addressability for object type 'partitioned table' +ok 41 - check addressability for object type 'policy' +ok 42 - check addressability for object type 'role' +ok 43 - check addressability for object type 'rule' +ok 44 - check addressability for object type 'schema' +ok 45 - check addressability for object type 'sequence' +ok 46 - check addressability for object type 'sequence column' +ok 47 - check addressability for object type 'server' +ok 48 - check addressability for object type 'table' +ok 49 - check addressability for object type 'table column' +ok 50 - check addressability for object type 'table constraint' +ok 51 - check addressability for object type 'tablespace' +ok 52 - check addressability for object type 'text search configuration' +ok 53 - check addressability for object type 'text search dictionary' +ok 54 - check addressability for object type 'text search parser' +ok 55 - check addressability for object type 'text search template' +ok 56 - check addressability for object type 'toast table' +ok 57 - check addressability for object type 'toast table column' +ok 58 - check addressability for object type 'transform' +ok 59 - check addressability for object type 'trigger' +ok 60 - check addressability for object type 'type' +ok 61 - check addressability for object type 'user mapping' +ok 62 - check addressability for object type 'view' +ok 63 - check addressability for object type 'view column' +ok 64 - lives_ok: SELECT * FROM cat_tools.object__catalog('access method') +ok 65 - lives_ok: SELECT * FROM cat_tools.object__catalog('aggregate') +ok 66 - lives_ok: SELECT * FROM cat_tools.object__catalog('cast') +ok 67 - lives_ok: SELECT * FROM cat_tools.object__catalog('collation') +ok 68 - lives_ok: SELECT * FROM cat_tools.object__catalog('composite type') +ok 69 - lives_ok: SELECT * FROM cat_tools.object__catalog('composite type column') +ok 70 - lives_ok: SELECT * FROM cat_tools.object__catalog('conversion') +ok 71 - lives_ok: SELECT * FROM cat_tools.object__catalog('database') +ok 72 - lives_ok: SELECT * FROM cat_tools.object__catalog('default acl') +ok 73 - lives_ok: SELECT * FROM cat_tools.object__catalog('default value') +ok 74 - lives_ok: SELECT * FROM cat_tools.object__catalog('domain constraint') +ok 75 - lives_ok: SELECT * FROM cat_tools.object__catalog('event trigger') +ok 76 - lives_ok: SELECT * FROM cat_tools.object__catalog('extension') +ok 77 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign table') +ok 78 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign table column') +ok 79 - lives_ok: SELECT * FROM cat_tools.object__catalog('foreign-data wrapper') +ok 80 - lives_ok: SELECT * FROM cat_tools.object__catalog('function') +ok 81 - lives_ok: SELECT * FROM cat_tools.object__catalog('function of access method') +ok 82 - lives_ok: SELECT * FROM cat_tools.object__catalog('index') +ok 83 - lives_ok: SELECT * FROM cat_tools.object__catalog('index column') +ok 84 - lives_ok: SELECT * FROM cat_tools.object__catalog('language') +ok 85 - lives_ok: SELECT * FROM cat_tools.object__catalog('large object') +ok 86 - lives_ok: SELECT * FROM cat_tools.object__catalog('materialized view') +ok 87 - lives_ok: SELECT * FROM cat_tools.object__catalog('materialized view column') +ok 88 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator') +ok 89 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator class') +ok 90 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator family') +ok 91 - lives_ok: SELECT * FROM cat_tools.object__catalog('operator of access method') +ok 92 - lives_ok: SELECT * FROM cat_tools.object__catalog('partitioned index') +ok 93 - lives_ok: SELECT * FROM cat_tools.object__catalog('partitioned table') +ok 94 - lives_ok: SELECT * FROM cat_tools.object__catalog('policy') +ok 95 - lives_ok: SELECT * FROM cat_tools.object__catalog('role') +ok 96 - lives_ok: SELECT * FROM cat_tools.object__catalog('rule') +ok 97 - lives_ok: SELECT * FROM cat_tools.object__catalog('schema') +ok 98 - lives_ok: SELECT * FROM cat_tools.object__catalog('sequence') +ok 99 - lives_ok: SELECT * FROM cat_tools.object__catalog('sequence column') +ok 100 - lives_ok: SELECT * FROM cat_tools.object__catalog('server') +ok 101 - lives_ok: SELECT * FROM cat_tools.object__catalog('table') +ok 102 - lives_ok: SELECT * FROM cat_tools.object__catalog('table column') +ok 103 - lives_ok: SELECT * FROM cat_tools.object__catalog('table constraint') +ok 104 - lives_ok: SELECT * FROM cat_tools.object__catalog('tablespace') +ok 105 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search configuration') +ok 106 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search dictionary') +ok 107 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search parser') +ok 108 - lives_ok: SELECT * FROM cat_tools.object__catalog('text search template') +ok 109 - lives_ok: SELECT * FROM cat_tools.object__catalog('toast table') +ok 110 - lives_ok: SELECT * FROM cat_tools.object__catalog('toast table column') +ok 111 - lives_ok: SELECT * FROM cat_tools.object__catalog('transform') +ok 112 - lives_ok: SELECT * FROM cat_tools.object__catalog('trigger') +ok 113 - lives_ok: SELECT * FROM cat_tools.object__catalog('type') +ok 114 - lives_ok: SELECT * FROM cat_tools.object__catalog('user mapping') +ok 115 - lives_ok: SELECT * FROM cat_tools.object__catalog('view') +ok 116 - lives_ok: SELECT * FROM cat_tools.object__catalog('view column') +ok 117 - lives_ok: SELECT * FROM cat_tools.object__reg_type('access method') +ok 118 - lives_ok: SELECT * FROM cat_tools.object__reg_type('aggregate') +ok 119 - lives_ok: SELECT * FROM cat_tools.object__reg_type('cast') +ok 120 - lives_ok: SELECT * FROM cat_tools.object__reg_type('collation') +ok 121 - lives_ok: SELECT * FROM cat_tools.object__reg_type('composite type') +ok 122 - lives_ok: SELECT * FROM cat_tools.object__reg_type('composite type column') +ok 123 - lives_ok: SELECT * FROM cat_tools.object__reg_type('conversion') +ok 124 - lives_ok: SELECT * FROM cat_tools.object__reg_type('database') +ok 125 - lives_ok: SELECT * FROM cat_tools.object__reg_type('default acl') +ok 126 - lives_ok: SELECT * FROM cat_tools.object__reg_type('default value') +ok 127 - lives_ok: SELECT * FROM cat_tools.object__reg_type('domain constraint') +ok 128 - lives_ok: SELECT * FROM cat_tools.object__reg_type('event trigger') +ok 129 - lives_ok: SELECT * FROM cat_tools.object__reg_type('extension') +ok 130 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign table') +ok 131 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign table column') +ok 132 - lives_ok: SELECT * FROM cat_tools.object__reg_type('foreign-data wrapper') +ok 133 - lives_ok: SELECT * FROM cat_tools.object__reg_type('function') +ok 134 - lives_ok: SELECT * FROM cat_tools.object__reg_type('function of access method') +ok 135 - lives_ok: SELECT * FROM cat_tools.object__reg_type('index') +ok 136 - lives_ok: SELECT * FROM cat_tools.object__reg_type('index column') +ok 137 - lives_ok: SELECT * FROM cat_tools.object__reg_type('language') +ok 138 - lives_ok: SELECT * FROM cat_tools.object__reg_type('large object') +ok 139 - lives_ok: SELECT * FROM cat_tools.object__reg_type('materialized view') +ok 140 - lives_ok: SELECT * FROM cat_tools.object__reg_type('materialized view column') +ok 141 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator') +ok 142 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator class') +ok 143 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator family') +ok 144 - lives_ok: SELECT * FROM cat_tools.object__reg_type('operator of access method') +ok 145 - lives_ok: SELECT * FROM cat_tools.object__reg_type('partitioned index') +ok 146 - lives_ok: SELECT * FROM cat_tools.object__reg_type('partitioned table') +ok 147 - lives_ok: SELECT * FROM cat_tools.object__reg_type('policy') +ok 148 - lives_ok: SELECT * FROM cat_tools.object__reg_type('role') +ok 149 - lives_ok: SELECT * FROM cat_tools.object__reg_type('rule') +ok 150 - lives_ok: SELECT * FROM cat_tools.object__reg_type('schema') +ok 151 - lives_ok: SELECT * FROM cat_tools.object__reg_type('sequence') +ok 152 - lives_ok: SELECT * FROM cat_tools.object__reg_type('sequence column') +ok 153 - lives_ok: SELECT * FROM cat_tools.object__reg_type('server') +ok 154 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table') +ok 155 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table column') +ok 156 - lives_ok: SELECT * FROM cat_tools.object__reg_type('table constraint') +ok 157 - lives_ok: SELECT * FROM cat_tools.object__reg_type('tablespace') +ok 158 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search configuration') +ok 159 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search dictionary') +ok 160 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search parser') +ok 161 - lives_ok: SELECT * FROM cat_tools.object__reg_type('text search template') +ok 162 - lives_ok: SELECT * FROM cat_tools.object__reg_type('toast table') +ok 163 - lives_ok: SELECT * FROM cat_tools.object__reg_type('toast table column') +ok 164 - lives_ok: SELECT * FROM cat_tools.object__reg_type('transform') +ok 165 - lives_ok: SELECT * FROM cat_tools.object__reg_type('trigger') +ok 166 - lives_ok: SELECT * FROM cat_tools.object__reg_type('type') +ok 167 - lives_ok: SELECT * FROM cat_tools.object__reg_type('user mapping') +ok 168 - lives_ok: SELECT * FROM cat_tools.object__reg_type('view') +ok 169 - lives_ok: SELECT * FROM cat_tools.object__reg_type('view column') +ok 170 - Verify cat_tools.object__address_classid('access method') +ok 171 - Verify cat_tools.object__address_classid('aggregate') +ok 172 - Verify cat_tools.object__address_classid('cast') +ok 173 - Verify cat_tools.object__address_classid('collation') +ok 174 - Verify cat_tools.object__address_classid('composite type') +ok 175 - Verify cat_tools.object__address_classid('composite type column') +ok 176 - Verify cat_tools.object__address_classid('conversion') +ok 177 - Verify cat_tools.object__address_classid('database') +ok 178 - Verify cat_tools.object__address_classid('default acl') +ok 179 - Verify cat_tools.object__address_classid('default value') +ok 180 - Verify cat_tools.object__address_classid('domain constraint') +ok 181 - Verify cat_tools.object__address_classid('event trigger') +ok 182 - Verify cat_tools.object__address_classid('extension') +ok 183 - Verify cat_tools.object__address_classid('foreign table') +ok 184 - Verify cat_tools.object__address_classid('foreign table column') +ok 185 - Verify cat_tools.object__address_classid('foreign-data wrapper') +ok 186 - Verify cat_tools.object__address_classid('function') +ok 187 - Verify cat_tools.object__address_classid('function of access method') +ok 188 - Verify cat_tools.object__address_classid('index') +ok 189 - Verify cat_tools.object__address_classid('index column') +ok 190 - Verify cat_tools.object__address_classid('language') +ok 191 - Verify cat_tools.object__address_classid('large object') +ok 192 - Verify cat_tools.object__address_classid('materialized view') +ok 193 - Verify cat_tools.object__address_classid('materialized view column') +ok 194 - Verify cat_tools.object__address_classid('operator') +ok 195 - Verify cat_tools.object__address_classid('operator class') +ok 196 - Verify cat_tools.object__address_classid('operator family') +ok 197 - Verify cat_tools.object__address_classid('operator of access method') +ok 198 - Verify cat_tools.object__address_classid('partitioned index') +ok 199 - Verify cat_tools.object__address_classid('partitioned table') +ok 200 - Verify cat_tools.object__address_classid('policy') +ok 201 - Verify cat_tools.object__address_classid('role') +ok 202 - Verify cat_tools.object__address_classid('rule') +ok 203 - Verify cat_tools.object__address_classid('schema') +ok 204 - Verify cat_tools.object__address_classid('sequence') +ok 205 - Verify cat_tools.object__address_classid('sequence column') +ok 206 - Verify cat_tools.object__address_classid('server') +ok 207 - Verify cat_tools.object__address_classid('table') +ok 208 - Verify cat_tools.object__address_classid('table column') +ok 209 - Verify cat_tools.object__address_classid('table constraint') +ok 210 - Verify cat_tools.object__address_classid('tablespace') +ok 211 - Verify cat_tools.object__address_classid('text search configuration') +ok 212 - Verify cat_tools.object__address_classid('text search dictionary') +ok 213 - Verify cat_tools.object__address_classid('text search parser') +ok 214 - Verify cat_tools.object__address_classid('text search template') +ok 215 - Verify cat_tools.object__address_classid('toast table') +ok 216 - Verify cat_tools.object__address_classid('toast table column') +ok 217 - Verify cat_tools.object__address_classid('transform') +ok 218 - Verify cat_tools.object__address_classid('trigger') +ok 219 - Verify cat_tools.object__address_classid('type') +ok 220 - Verify cat_tools.object__address_classid('user mapping') +ok 221 - Verify cat_tools.object__address_classid('view') +ok 222 - Verify cat_tools.object__address_classid('view column') +ok 223 - Change search_path +ok 224 - Create bogus pg_class table +ok 225 - Create bogus regclass type +ok 226 - Simple 'pg_class'::pg_catalog.regclass should not return pg_catalog.pg_class +ok 227 - Simple 'regclass'::regtype should not return pg_catalog.regtype +ok 228 - cat_tools.object__catalog('table') returns pg_catalog.pg_class +ok 229 - cat_tools.object__catalog('table') returns pg_catalog.pg_class +ok 230 - Verify objects__shared_src() returns correct values # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/permissions.out b/test/expected/permissions.out new file mode 100644 index 0000000..4984217 --- /dev/null +++ b/test/expected/permissions.out @@ -0,0 +1,163 @@ +\set ECHO none +1..160 +ok 1 - Permission denied trying to execute cat_tools.currval(table_name text, column_name text) +ok 2 - Permission denied trying to execute cat_tools.enum_range(enum regtype) +ok 3 - Permission denied trying to execute cat_tools.enum_range_srf(enum regtype) +ok 4 - Permission denied trying to execute cat_tools.extension__schemas(extension_names name[]) +ok 5 - Permission denied trying to execute cat_tools.extension__schemas(extension_names text) +ok 6 - Permission denied trying to execute cat_tools.extension__schemas_unique(extension_names name[]) +ok 7 - Permission denied trying to execute cat_tools.extension__schemas_unique(extension_names text) +ok 8 - Permission denied trying to execute cat_tools.function__arg_types(arguments text) +ok 9 - Permission denied trying to execute cat_tools.function__arg_types_text(arguments text) +ok 10 - Permission denied trying to execute cat_tools.get_serial_sequence(table_name text, column_name text) +ok 11 - Permission denied trying to execute cat_tools.name__check(name_to_check text) +ok 12 - Permission denied trying to execute cat_tools.nextval(table_name text, column_name text) +ok 13 - Permission denied trying to execute cat_tools.object__address_classid(object_type cat_tools.object_type) +ok 14 - Permission denied trying to execute cat_tools.object__address_classid(object_type text) +ok 15 - Permission denied trying to execute cat_tools.object__catalog(object_type cat_tools.object_type) +ok 16 - Permission denied trying to execute cat_tools.object__catalog(object_type text) +ok 17 - Permission denied trying to execute cat_tools.object__is_address_unsupported(object_type cat_tools.object_type) +ok 18 - Permission denied trying to execute cat_tools.object__is_address_unsupported(object_type text) +ok 19 - Permission denied trying to execute cat_tools.object__is_shared(object_type cat_tools.object_type) +ok 20 - Permission denied trying to execute cat_tools.object__is_shared(object_type text) +ok 21 - Permission denied trying to execute cat_tools.object__reg_type(object_catalog regclass) +ok 22 - Permission denied trying to execute cat_tools.object__reg_type(object_type cat_tools.object_type) +ok 23 - Permission denied trying to execute cat_tools.object__reg_type(object_type text) +ok 24 - Permission denied trying to execute cat_tools.object__reg_type_catalog(object_identifier_type regtype) +ok 25 - Permission denied trying to execute cat_tools.objects__address_unsupported() +ok 26 - Permission denied trying to execute cat_tools.objects__address_unsupported_srf() +ok 27 - Permission denied trying to execute cat_tools.objects__shared() +ok 28 - Permission denied trying to execute cat_tools.objects__shared_srf() +ok 29 - Permission denied trying to execute cat_tools.pg_attribute__get(relation regclass, column_name name) +ok 30 - Permission denied trying to execute cat_tools.pg_class(rel regclass) +ok 31 - Permission denied trying to execute cat_tools.pg_extension__get(extension_name name) +ok 32 - Permission denied trying to execute cat_tools.regprocedure(function_name text, arguments text) +ok 33 - Permission denied trying to execute cat_tools.relation__column_names(relation regclass) +ok 34 - Permission denied trying to execute cat_tools.relation__is_catalog(relation regclass) +ok 35 - Permission denied trying to execute cat_tools.relation__is_temp(relation regclass) +ok 36 - Permission denied trying to execute cat_tools.relation__kind(relkind cat_tools.relation_relkind) +ok 37 - Permission denied trying to execute cat_tools.relation__kind(relkind text) +ok 38 - Permission denied trying to execute cat_tools.relation__relkind(kind cat_tools.relation_type) +ok 39 - Permission denied trying to execute cat_tools.relation__relkind(kind text) +ok 40 - Permission denied trying to execute cat_tools.routine__arg_names(func regprocedure) +ok 41 - Permission denied trying to execute cat_tools.routine__arg_names_text(func regprocedure) +ok 42 - Permission denied trying to execute cat_tools.routine__arg_types(func regprocedure) +ok 43 - Permission denied trying to execute cat_tools.routine__arg_types_text(func regprocedure) +ok 44 - Permission denied trying to execute cat_tools.routine__argument_mode(proargmode cat_tools.routine_proargmode) +ok 45 - Permission denied trying to execute cat_tools.routine__parallel_safety(proparallel cat_tools.routine_proparallel) +ok 46 - Permission denied trying to execute cat_tools.routine__parse_arg_names(arguments text) +ok 47 - Permission denied trying to execute cat_tools.routine__parse_arg_names_text(arguments text) +ok 48 - Permission denied trying to execute cat_tools.routine__parse_arg_types(arguments text) +ok 49 - Permission denied trying to execute cat_tools.routine__parse_arg_types_text(arguments text) +ok 50 - Permission denied trying to execute cat_tools.routine__type(prokind cat_tools.routine_prokind) +ok 51 - Permission denied trying to execute cat_tools.routine__volatility(provolatile cat_tools.routine_provolatile) +ok 52 - Permission denied trying to execute cat_tools.sequence__last(table_name text, column_name text) +ok 53 - Permission denied trying to execute cat_tools.sequence__next(table_name text, column_name text) +ok 54 - Permission denied trying to execute cat_tools.sequence__set_last(table_name text, column_name text, last_value bigint) +ok 55 - Permission denied trying to execute cat_tools.sequence__set_next(table_name text, column_name text, next_value bigint) +ok 56 - Permission denied trying to execute cat_tools.setval(table_name text, column_name text, new_value bigint, has_been_used boolean DEFAULT true) +ok 57 - Permission denied trying to execute cat_tools.trigger__args_as_text(function_arguments text[]) +ok 58 - Permission denied trying to execute cat_tools.trigger__get_oid(trigger_table regclass, trigger_name text) +ok 59 - Permission denied trying to execute cat_tools.trigger__get_oid__loose(trigger_table regclass, trigger_name text) +ok 60 - Permission denied trying to execute 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[]) +ok 61 - Permission denied trying to execute cat_tools.trigger__parse(trigger_table 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[]) +ok 62 - Permission granted to execute cat_tools.currval(table_name text, column_name text) +ok 63 - Permission granted to execute cat_tools.enum_range(enum regtype) +ok 64 - Permission granted to execute cat_tools.enum_range_srf(enum regtype) +ok 65 - Permission granted to execute cat_tools.extension__schemas(extension_names name[]) +ok 66 - Permission granted to execute cat_tools.extension__schemas(extension_names text) +ok 67 - Permission granted to execute cat_tools.extension__schemas_unique(extension_names name[]) +ok 68 - Permission granted to execute cat_tools.extension__schemas_unique(extension_names text) +ok 69 - Permission granted to execute cat_tools.function__arg_types(arguments text) +ok 70 - Permission granted to execute cat_tools.function__arg_types_text(arguments text) +ok 71 - Permission granted to execute cat_tools.get_serial_sequence(table_name text, column_name text) +ok 72 - Permission granted to execute cat_tools.name__check(name_to_check text) +ok 73 - Permission granted to execute cat_tools.nextval(table_name text, column_name text) +ok 74 - Permission granted to execute cat_tools.object__address_classid(object_type cat_tools.object_type) +ok 75 - Permission granted to execute cat_tools.object__address_classid(object_type text) +ok 76 - Permission granted to execute cat_tools.object__catalog(object_type cat_tools.object_type) +ok 77 - Permission granted to execute cat_tools.object__catalog(object_type text) +ok 78 - Permission granted to execute cat_tools.object__is_address_unsupported(object_type cat_tools.object_type) +ok 79 - Permission granted to execute cat_tools.object__is_address_unsupported(object_type text) +ok 80 - Permission granted to execute cat_tools.object__is_shared(object_type cat_tools.object_type) +ok 81 - Permission granted to execute cat_tools.object__is_shared(object_type text) +ok 82 - Permission granted to execute cat_tools.object__reg_type(object_catalog regclass) +ok 83 - Permission granted to execute cat_tools.object__reg_type(object_type cat_tools.object_type) +ok 84 - Permission granted to execute cat_tools.object__reg_type(object_type text) +ok 85 - Permission granted to execute cat_tools.object__reg_type_catalog(object_identifier_type regtype) +ok 86 - Permission granted to execute cat_tools.objects__address_unsupported() +ok 87 - Permission granted to execute cat_tools.objects__address_unsupported_srf() +ok 88 - Permission granted to execute cat_tools.objects__shared() +ok 89 - Permission granted to execute cat_tools.objects__shared_srf() +ok 90 - Permission granted to execute cat_tools.pg_attribute__get(relation regclass, column_name name) +ok 91 - Permission granted to execute cat_tools.pg_class(rel regclass) +ok 92 - Permission granted to execute cat_tools.pg_extension__get(extension_name name) +ok 93 - Permission granted to execute cat_tools.regprocedure(function_name text, arguments text) +ok 94 - Permission granted to execute cat_tools.relation__column_names(relation regclass) +ok 95 - Permission granted to execute cat_tools.relation__is_catalog(relation regclass) +ok 96 - Permission granted to execute cat_tools.relation__is_temp(relation regclass) +ok 97 - Permission granted to execute cat_tools.relation__kind(relkind cat_tools.relation_relkind) +ok 98 - Permission granted to execute cat_tools.relation__kind(relkind text) +ok 99 - Permission granted to execute cat_tools.relation__relkind(kind cat_tools.relation_type) +ok 100 - Permission granted to execute cat_tools.relation__relkind(kind text) +ok 101 - Permission granted to execute cat_tools.routine__arg_names(func regprocedure) +ok 102 - Permission granted to execute cat_tools.routine__arg_names_text(func regprocedure) +ok 103 - Permission granted to execute cat_tools.routine__arg_types(func regprocedure) +ok 104 - Permission granted to execute cat_tools.routine__arg_types_text(func regprocedure) +ok 105 - Permission granted to execute cat_tools.routine__argument_mode(proargmode cat_tools.routine_proargmode) +ok 106 - Permission granted to execute cat_tools.routine__parallel_safety(proparallel cat_tools.routine_proparallel) +ok 107 - Permission granted to execute cat_tools.routine__parse_arg_names(arguments text) +ok 108 - Permission granted to execute cat_tools.routine__parse_arg_names_text(arguments text) +ok 109 - Permission granted to execute cat_tools.routine__parse_arg_types(arguments text) +ok 110 - Permission granted to execute cat_tools.routine__parse_arg_types_text(arguments text) +ok 111 - Permission granted to execute cat_tools.routine__type(prokind cat_tools.routine_prokind) +ok 112 - Permission granted to execute cat_tools.routine__volatility(provolatile cat_tools.routine_provolatile) +ok 113 - Permission granted to execute cat_tools.sequence__last(table_name text, column_name text) +ok 114 - Permission granted to execute cat_tools.sequence__next(table_name text, column_name text) +ok 115 - Permission granted to execute cat_tools.sequence__set_last(table_name text, column_name text, last_value bigint) +ok 116 - Permission granted to execute cat_tools.sequence__set_next(table_name text, column_name text, next_value bigint) +ok 117 - Permission granted to execute cat_tools.setval(table_name text, column_name text, new_value bigint, has_been_used boolean DEFAULT true) +ok 118 - Permission granted to execute cat_tools.trigger__args_as_text(function_arguments text[]) +ok 119 - Permission granted to execute cat_tools.trigger__get_oid(trigger_table regclass, trigger_name text) +ok 120 - Permission granted to execute cat_tools.trigger__get_oid__loose(trigger_table regclass, trigger_name text) +ok 121 - Permission granted to execute 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[]) +ok 122 - Permission granted to execute cat_tools.trigger__parse(trigger_table 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[]) +ok 123 - Permission denied trying to use type cat_tools.column +ok 124 - Permission denied trying to use type cat_tools.constraint_type +ok 125 - Permission denied trying to use type cat_tools.object_type +ok 126 - Permission denied trying to use type cat_tools.pg_all_foreign_keys +ok 127 - Permission denied trying to use type cat_tools.pg_class_v +ok 128 - Permission denied trying to use type cat_tools.pg_extension_v +ok 129 - Permission denied trying to use type cat_tools.procedure_type +ok 130 - Permission denied trying to use type cat_tools.relation_relkind +ok 131 - Permission denied trying to use type cat_tools.relation_type +ok 132 - Permission denied trying to use type cat_tools.routine_argument +ok 133 - Permission denied trying to use type cat_tools.routine_argument_mode +ok 134 - Permission denied trying to use type cat_tools.routine_parallel_safety +ok 135 - Permission denied trying to use type cat_tools.routine_proargmode +ok 136 - Permission denied trying to use type cat_tools.routine_prokind +ok 137 - Permission denied trying to use type cat_tools.routine_proparallel +ok 138 - Permission denied trying to use type cat_tools.routine_provolatile +ok 139 - Permission denied trying to use type cat_tools.routine_type +ok 140 - Permission denied trying to use type cat_tools.routine_volatility +ok 141 - Permission granted to use type cat_tools.column +ok 142 - Permission granted to use type cat_tools.constraint_type +ok 143 - Permission granted to use type cat_tools.object_type +ok 144 - Permission granted to use type cat_tools.pg_all_foreign_keys +ok 145 - Permission granted to use type cat_tools.pg_class_v +ok 146 - Permission granted to use type cat_tools.pg_extension_v +ok 147 - Permission granted to use type cat_tools.procedure_type +ok 148 - Permission granted to use type cat_tools.relation_relkind +ok 149 - Permission granted to use type cat_tools.relation_type +ok 150 - Permission granted to use type cat_tools.routine_argument +ok 151 - Permission granted to use type cat_tools.routine_argument_mode +ok 152 - Permission granted to use type cat_tools.routine_parallel_safety +ok 153 - Permission granted to use type cat_tools.routine_proargmode +ok 154 - Permission granted to use type cat_tools.routine_prokind +ok 155 - Permission granted to use type cat_tools.routine_proparallel +ok 156 - Permission granted to use type cat_tools.routine_provolatile +ok 157 - Permission granted to use type cat_tools.routine_type +ok 158 - Permission granted to use type cat_tools.routine_volatility +ok 159 - no_use_role has no USAGE on _cat_tools schema +ok 160 - use_role has USAGE on _cat_tools schema +# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/relation__.out b/test/expected/relation__.out new file mode 100644 index 0000000..ff14a93 --- /dev/null +++ b/test/expected/relation__.out @@ -0,0 +1,41 @@ +\set ECHO none +1..38 +ok 1 - Verify count from kinds +ok 2 - Simple sanity check of relation__kind() +ok 3 - Simple sanity check of relation__relkind() +ok 4 - SELECT cat_tools.relation_relkind('table') +ok 5 - SELECT cat_tools.relation_relkind('index') +ok 6 - SELECT cat_tools.relation_relkind('sequence') +ok 7 - SELECT cat_tools.relation_relkind('toast table') +ok 8 - SELECT cat_tools.relation_relkind('view') +ok 9 - SELECT cat_tools.relation_relkind('composite type') +ok 10 - SELECT cat_tools.relation_relkind('foreign table') +ok 11 - SELECT cat_tools.relation_relkind('materialized view') +ok 12 - SELECT cat_tools.relation_relkind('partitioned table') +ok 13 - SELECT cat_tools.relation_relkind('partitioned index') +ok 14 - SELECT cat_tools.relation_type('r') +ok 15 - SELECT cat_tools.relation_type('i') +ok 16 - SELECT cat_tools.relation_type('S') +ok 17 - SELECT cat_tools.relation_type('t') +ok 18 - SELECT cat_tools.relation_type('v') +ok 19 - SELECT cat_tools.relation_type('c') +ok 20 - SELECT cat_tools.relation_type('f') +ok 21 - SELECT cat_tools.relation_type('m') +ok 22 - SELECT cat_tools.relation_type('p') +ok 23 - SELECT cat_tools.relation_type('I') +ok 24 - cat_tools handles every relkind defined in pg_class.h +ok 25 - Verify public has no perms +ok 26 - pg_catalog.pg_class is not a temp relation +ok 27 - Create shared temp table for is_temp/is_catalog/column_names tests +ok 28 - temp relation is correctly identified as temp +ok 29 - NULL input returns NULL (STRICT function) +ok 30 - Verify public has no perms +ok 31 - pg_catalog.pg_class is in pg_catalog schema +ok 32 - temp relation is not in pg_catalog schema +ok 33 - NULL input returns NULL (STRICT function) +ok 34 - Verify public has no perms +ok 35 - Temp table returns expected column names +ok 36 - Drop middle column from temp table +ok 37 - Temp table with dropped column returns expected column names +ok 38 - NULL input returns NULL (STRICT function) +# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/relation_type.out b/test/expected/relation_type.out deleted file mode 100644 index c8d4172..0000000 --- a/test/expected/relation_type.out +++ /dev/null @@ -1,34 +0,0 @@ -\set ECHO none -1..31 -ok 1 - Verify count from kinds -ok 2 - Simple sanity check of relation__kind() -ok 3 - Simple sanity check of relation__relkind() -ok 4 - Permission denied trying to use types -ok 5 - Permission denied trying to use types -ok 6 - Permission denied trying to run functions -ok 7 - Permission denied trying to run functions -ok 8 - SELECT cat_tools.relation_relkind('table') -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 16 - SELECT cat_tools.relation_type('r') -ok 17 - SELECT cat_tools.relation_type('i') -ok 18 - SELECT cat_tools.relation_type('S') -ok 19 - SELECT cat_tools.relation_type('t') -ok 20 - SELECT cat_tools.relation_type('v') -ok 21 - SELECT cat_tools.relation_type('c') -ok 22 - SELECT cat_tools.relation_type('f') -ok 23 - SELECT cat_tools.relation_type('m') -ok 24 - SELECT cat_tools.relation_type('r'::"char") -ok 25 - SELECT cat_tools.relation_type('i'::"char") -ok 26 - SELECT cat_tools.relation_type('S'::"char") -ok 27 - SELECT cat_tools.relation_type('t'::"char") -ok 28 - SELECT cat_tools.relation_type('v'::"char") -ok 29 - SELECT cat_tools.relation_type('c'::"char") -ok 30 - SELECT cat_tools.relation_type('f'::"char") -ok 31 - SELECT cat_tools.relation_type('m'::"char") -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/routine__.out b/test/expected/routine__.out new file mode 100644 index 0000000..e9736f0 --- /dev/null +++ b/test/expected/routine__.out @@ -0,0 +1,92 @@ +\set ECHO none +1..89 +ok 1 - Verify type and prokind enums have same size +ok 2 - Simple sanity check of routine__type() +ok 3 - Simple sanity check of routine__type() with enum +ok 4 - SELECT cat_tools.routine__type('f'::cat_tools.routine_prokind) +ok 5 - SELECT cat_tools.routine__type('p'::cat_tools.routine_prokind) +ok 6 - SELECT cat_tools.routine__type('a'::cat_tools.routine_prokind) +ok 7 - SELECT cat_tools.routine__type('w'::cat_tools.routine_prokind) +ok 8 - SELECT cat_tools.routine__type('f'::"char") +ok 9 - SELECT cat_tools.routine__type('p'::"char") +ok 10 - SELECT cat_tools.routine__type('a'::"char") +ok 11 - SELECT cat_tools.routine__type('w'::"char") +ok 12 - SELECT cat_tools.routine__type('f') +ok 13 - SELECT cat_tools.routine__type('p') +ok 14 - SELECT cat_tools.routine__type('a') +ok 15 - SELECT cat_tools.routine__type('w') +ok 16 - Verify argument_mode and proargmode enums have same size +ok 17 - Simple sanity check of routine__argument_mode() +ok 18 - Simple sanity check of routine__argument_mode() with enum +ok 19 - SELECT cat_tools.routine__argument_mode('i'::cat_tools.routine_proargmode) +ok 20 - SELECT cat_tools.routine__argument_mode('o'::cat_tools.routine_proargmode) +ok 21 - SELECT cat_tools.routine__argument_mode('b'::cat_tools.routine_proargmode) +ok 22 - SELECT cat_tools.routine__argument_mode('v'::cat_tools.routine_proargmode) +ok 23 - SELECT cat_tools.routine__argument_mode('t'::cat_tools.routine_proargmode) +ok 24 - SELECT cat_tools.routine__argument_mode('i'::"char") +ok 25 - SELECT cat_tools.routine__argument_mode('o'::"char") +ok 26 - SELECT cat_tools.routine__argument_mode('b'::"char") +ok 27 - SELECT cat_tools.routine__argument_mode('v'::"char") +ok 28 - SELECT cat_tools.routine__argument_mode('t'::"char") +ok 29 - SELECT cat_tools.routine__argument_mode('i') +ok 30 - SELECT cat_tools.routine__argument_mode('o') +ok 31 - SELECT cat_tools.routine__argument_mode('b') +ok 32 - SELECT cat_tools.routine__argument_mode('v') +ok 33 - SELECT cat_tools.routine__argument_mode('t') +ok 34 - Verify volatility and provolatile enums have same size +ok 35 - Simple sanity check of routine__volatility() +ok 36 - Simple sanity check of routine__volatility() with enum +ok 37 - SELECT cat_tools.routine__volatility('i'::cat_tools.routine_provolatile) +ok 38 - SELECT cat_tools.routine__volatility('s'::cat_tools.routine_provolatile) +ok 39 - SELECT cat_tools.routine__volatility('v'::cat_tools.routine_provolatile) +ok 40 - SELECT cat_tools.routine__volatility('i'::"char") +ok 41 - SELECT cat_tools.routine__volatility('s'::"char") +ok 42 - SELECT cat_tools.routine__volatility('v'::"char") +ok 43 - SELECT cat_tools.routine__volatility('i') +ok 44 - SELECT cat_tools.routine__volatility('s') +ok 45 - SELECT cat_tools.routine__volatility('v') +ok 46 - Verify parallel_safety and proparallel enums have same size +ok 47 - Simple sanity check of routine__parallel_safety() +ok 48 - Simple sanity check of routine__parallel_safety() with enum +ok 49 - SELECT cat_tools.routine__parallel_safety('s'::cat_tools.routine_proparallel) +ok 50 - SELECT cat_tools.routine__parallel_safety('r'::cat_tools.routine_proparallel) +ok 51 - SELECT cat_tools.routine__parallel_safety('u'::cat_tools.routine_proparallel) +ok 52 - SELECT cat_tools.routine__parallel_safety('s'::"char") +ok 53 - SELECT cat_tools.routine__parallel_safety('r'::"char") +ok 54 - SELECT cat_tools.routine__parallel_safety('u'::"char") +ok 55 - SELECT cat_tools.routine__parallel_safety('s') +ok 56 - SELECT cat_tools.routine__parallel_safety('r') +ok 57 - SELECT cat_tools.routine__parallel_safety('u') +ok 58 - Verify public has no perms +ok 59 - Verify public has no perms +ok 60 - Security check should prevent execution when current_user != session_user +ok 61 - Verify routine__parse_arg_types() with INOUT and OUT +ok 62 - Verify routine__parse_arg_types() with just INOUT +ok 63 - Verify routine__parse_arg_types() with just OUT +ok 64 - Verify routine__parse_arg_types() with only inputs +ok 65 - Function cat_tools.routine__parse_arg_types(text) should not be security definer +ok 66 - Function cat_tools.routine__parse_arg_types_text(text) should not be security definer +ok 67 - Create pg_temp.test_function(anyarray, OUT text, OUT "char", pg_class, int, VARIADIC boolean[]) +ok 68 - Verify routine__arg_types() returns all argument types +ok 69 - Verify routine__arg_types() with IN arguments only +ok 70 - Verify routine__arg_types() with no arguments +ok 71 - Verify routine__arg_types() with VARIADIC argument +ok 72 - Verify routine__arg_types_text() formatting +ok 73 - Verify routine__arg_types_text() with simple types +ok 74 - Verify routine__arg_types_text() with no arguments +ok 75 - Verify routine__arg_types_text() with VARIADIC +ok 76 - Verify routine__parse_arg_names() with INOUT and OUT +ok 77 - Verify routine__parse_arg_names() with just INOUT +ok 78 - Verify routine__parse_arg_names() with just OUT +ok 79 - Verify routine__parse_arg_names() with only inputs +ok 80 - Function cat_tools.routine__parse_arg_names(text) should not be security definer +ok 81 - Function cat_tools.routine__parse_arg_names_text(text) should not be security definer +ok 82 - Create pg_temp.named_function(input_val int, INOUT inout_val text, OUT output_val boolean) +ok 83 - Verify routine__arg_names() returns argument names (unnamed function) +ok 84 - Verify routine__arg_names() with named arguments +ok 85 - Verify routine__arg_names() with no arguments +ok 86 - Verify routine__arg_names_text() formatting +ok 87 - Verify routine__arg_names_text() with unnamed arguments +ok 88 - Verify routine__arg_names_text() with built-in function +ok 89 - Verify routine__arg_names_text() with no arguments +# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/gen-relkinds.sh b/test/gen-relkinds.sh new file mode 100755 index 0000000..56a6830 --- /dev/null +++ b/test/gen-relkinds.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# test/gen-relkinds.sh - emit a psql script defining the canonical set of +# pg_class.relkind values extracted from the PostgreSQL server header we are +# building against. +# +# Usage: test/gen-relkinds.sh /path/to/catalog/pg_class.h +# +# The output is \i'd by test/sql/relation__.sql, which then asserts that every +# relkind PostgreSQL defines is known to cat_tools.relation_relkind. This lets +# the test suite detect a PostgreSQL version that ADDS a new relkind (or renames +# one) that cat_tools does not yet handle. +# +# If the header is not readable (e.g. postgresql-server-dev-NN is not installed) +# the script emits an EMPTY view, so the drift check in relation__.sql passes +# vacuously (zero unknown relkinds) with identical output -- `make test` must +# still work, and produce the same expected output, without server headers. +set -euo pipefail + +header="${1:-}" + +echo "-- GENERATED by test/gen-relkinds.sh from pg_class.h. DO NOT EDIT." + +if [ -n "$header" ] && [ -r "$header" ]; then + echo "CREATE TEMP VIEW pg_class_relkind_source (relkind, macro, description) AS" + echo "VALUES" + # Match: #define RELKIND_NAME 'x' /* comment */ + # Skip the function-like macros (RELKIND_HAS_STORAGE(relkind) etc.): those + # have '(' after the name instead of whitespace + a quoted char literal. + # \047 is a single quote, used throughout to avoid shell-quoting grief. + awk ' + /#define[ \t]+RELKIND_[A-Z_]+[ \t]+\047.\047/ { + name = $2 + i = index($0, "\047") + ch = substr($0, i + 1, 1) + desc = "" + if (match($0, /\/\*.*\*\//)) { + desc = substr($0, RSTART + 2, RLENGTH - 4) + gsub(/^[ \t]+|[ \t]+$/, "", desc) + } + # Double any embedded single quotes for SQL literals. + gsub(/\047/, "\047\047", ch) + gsub(/\047/, "\047\047", name) + gsub(/\047/, "\047\047", desc) + sep = (n++ == 0) ? " " : ", " + printf " %s(\047%s\047, \047%s\047, \047%s\047)\n", sep, ch, name, desc + } + ' "$header" + echo ";" +else + echo "CREATE TEMP VIEW pg_class_relkind_source (relkind, macro, description) AS" + echo " SELECT NULL::text, NULL::text, NULL::text WHERE false;" +fi diff --git a/test/helpers/enum_mapping.sql b/test/helpers/enum_mapping.sql new file mode 100644 index 0000000..b3bf9a4 --- /dev/null +++ b/test/helpers/enum_mapping.sql @@ -0,0 +1,103 @@ +/* + * Enum mapping helper: tests two sets of roundtrip behavior for a pair of + * "char"-based and human-readable enums, preceded by a size check. + * + * Set 1 - Individual example (fully specified): + * Tests with a known sample value (sample_char -> sample_text), verifying + * that the mapping function returns exactly the right result. This is the + * most meaningful correctness check because the expected output is + * externally specified rather than derived from the enum itself. + * + * Set 2 - All-values coverage (simpler roundtrip): + * For every value in both enums (via _enum_pairs view), verifies the + * mapping function does not return NULL/error when called with the enum + * type, the "char" type, and a plain text argument. Breadth over depth. + * + * Required variables (set before \i): + * s - schema name (typically already set globally, e.g. cat_tools) + * kind - base name (e.g. type) + * char_col - pg catalog column name (e.g. prokind) + * sample_char - one sample "char" value (e.g. f) + * sample_text - expected text result for sample_char (e.g. function) + * + * Internal variables (built here and unset at end): + * __f, __enum_type, __char_enum_type + */ +SELECT + 'routine__' || :'kind' AS __f + , 'cat_tools.routine_' || :'kind' AS __enum_type + , 'cat_tools.routine_' || :'char_col' AS __char_enum_type +\gset + +CREATE OR REPLACE TEMP VIEW _enum_pairs AS + SELECT + (cat_tools.enum_range(:'__char_enum_type'))[gs]::text AS char_val + , (cat_tools.enum_range(:'__enum_type'))[gs]::text AS text_val + FROM generate_series( + 1 + , greatest( + /* + * The two enums should be the same size; greatest() is used to + * iterate both so that if they ever diverge we get a NULL failure + * on the extra row rather than silently missing entries. + * The size match is explicitly validated in Set 1 below. + */ + array_length(cat_tools.enum_range(:'__enum_type'), 1) + , array_length(cat_tools.enum_range(:'__char_enum_type'), 1) + ) + ) gs +; + +-- Prerequisite: both enums must be the same size + +SELECT is( + array_length(cat_tools.enum_range(:'__enum_type'), 1) + , array_length(cat_tools.enum_range(:'__char_enum_type'), 1) + , 'Verify ' || :'kind' || ' and ' || :'char_col' || ' enums have same size' + ); + +-- Set 1: Individual example with fully specified expected value + +SELECT is( + :s.routine__:kind(:'sample_char') + , :'sample_text' + , 'Simple sanity check of ' || :'__f' || '()' + ); + +SELECT is( + :s.routine__:kind(:'sample_char':::__char_enum_type) + , :'sample_text' + , 'Simple sanity check of ' || :'__f' || '() with enum' + ); + +-- Set 2: All-values coverage (simpler roundtrip across every enum entry) + +SELECT is( + :s.routine__:kind(char_val:::__char_enum_type)::text + , text_val + , format('SELECT ' || :'s' || '.' || :'__f' || '(%L::' || :'__char_enum_type' || ')', char_val) + ) + FROM _enum_pairs +; + +SELECT is( + :s.routine__:kind(char_val::"char")::text + , text_val + , format('SELECT ' || :'s' || '.' || :'__f' || '(%L::"char")', char_val) + ) + FROM _enum_pairs +; + +SELECT is( + :s.routine__:kind(char_val::"char")::text + , text_val + , format('SELECT ' || :'s' || '.' || :'__f' || '(%L)', char_val) + ) + FROM _enum_pairs +; + +\unset __f +\unset __enum_type +\unset __char_enum_type + +-- vi: expandtab ts=2 sw=2 diff --git a/test/install/.gitignore b/test/install/.gitignore new file mode 100644 index 0000000..eb02f57 --- /dev/null +++ b/test/install/.gitignore @@ -0,0 +1,6 @@ +# pg_regress writes the install step's result (and any diff) here, because the +# install schedule references tests as ../install/. The install output is +# self-comparing (pg_regress resolves both the expected and result paths to this +# directory), so it is never asserted and must not be tracked. +load.out +install.out.diff diff --git a/test/install/load.sql b/test/install/load.sql new file mode 100644 index 0000000..f0fd25a --- /dev/null +++ b/test/install/load.sql @@ -0,0 +1,202 @@ +/* + * Single, committed-once installer for the test suite's dependencies: the + * cat_tools extension (see the modes below) and the test roles + grants. + * + * pgxntool's test/install feature runs this file COMMITTED, in its own + * pg_regress session, BEFORE the main pgTAP suite. Because its state is + * committed it persists into every test and runs ONCE instead of per-test + * (pgTAP rolls back each test/sql/ file, so tests read these objects but never + * modify them). Committing also MATTERS for correctness in update mode: the + * update to the current version runs ALTER TYPE ... ADD VALUE on + * cat_tools.relation_relkind / relation_type, and PostgreSQL forbids USING a + * newly added enum value in the same transaction that added it (SQLSTATE 55P04, + * "unsafe use of new value"). The suite uses those values, so the update must be + * committed before the suite runs -- mirroring a real production update (ALTER + * EXTENSION UPDATE commits, then later transactions use the new values). + * deps.sql (run per-test) installs nothing; it only sets the psql variables the + * suite references. + * + * Three modes, selected by the cat_tools.test_load_mode placeholder GUC, which + * the Makefile TEST_LOAD_SOURCE block sets via PGOPTIONS (fresh is the default): + * - fresh (default): plain CREATE EXTENSION cat_tools (current version). + * - update: CREATE EXTENSION at an older version (cat_tools.test_update_from, + * default 0.2.2) then ALTER EXTENSION UPDATE -- to cat_tools.test_update_to + * when that GUC is non-empty, otherwise to the current default_version. + * Reusing the SAME suite and expected output asserts an updated database + * behaves identically to a fresh install. + * - existing: the extension is ALREADY installed (by binary pg_upgrade, or an + * ALTER EXTENSION UPDATE performed outside the suite). load.sql must NOT + * drop/create/update it -- that would destroy exactly what the suite + * validates. It only asserts presence + current version, then creates the + * test roles. + * + * Version floors: + * - 0.2.2 is the OLDEST cat_tools version that installs cleanly on the + * supported PostgreSQL range (PG12+); the 0.2.0/0.2.1 install scripts fail + * on PG11+/PG12+. It is the default update-from floor -- the WIDEST update + * path we can exercise here -- not a claim that we only care about 0.2.2. + * - PG12 is the PostgreSQL floor: ALTER TYPE ... ADD VALUE cannot run inside + * a transaction block (or an extension update script) at all before PG12. + */ +SET client_min_messages = WARNING; + +/* + * The test-role names come from test/roles.sql (the single source of truth, + * also loaded per-test by test/deps.sql). Each :var holds the RAW identifier; + * we quote it at every use site (:"use_role" for CREATE ROLE / GRANT, :'use_role' + * for the pg_temp helper text arguments). + */ +\i test/roles.sql + +/* + * Mode selection. The Makefile always exports cat_tools.test_load_mode via + * PGOPTIONS. Read it WITHOUT missing_ok: if the GUC did not propagate (a break + * anywhere in make -> PGOPTIONS -> env -> psql), current_setting errors here and + * the whole install step fails loudly, instead of silently falling back to a + * default and running the wrong suite. The DO block then rejects any value + * other than fresh/update/existing with a clear message. + */ +SELECT current_setting('cat_tools.test_load_mode') AS cat_tools_test_load_mode +\gset + +DO $DO$ +BEGIN + IF current_setting('cat_tools.test_load_mode') NOT IN ('fresh', 'update', 'existing') THEN + RAISE EXCEPTION + 'cat_tools.test_load_mode must be ''fresh'', ''update'' or ''existing'', got ''%''' + , current_setting('cat_tools.test_load_mode') + ; + END IF; +END +$DO$; + +SELECT + :'cat_tools_test_load_mode' = 'update' AS cat_tools_mode_update + , :'cat_tools_test_load_mode' = 'existing' AS cat_tools_mode_existing +\gset + +\if :cat_tools_mode_existing +/* + * existing mode: do NOT touch the extension. Assert it is installed and at the + * current default_version -- the pg_upgrade / external update the database just + * went through is exactly what the suite is validating, so dropping or + * reinstalling it would defeat the test. Fail loudly on absence or mismatch. + * (CI additionally plants a dependency guard so a stray non-CASCADE drop would + * error rather than silently reinstall; see bin/test_existing.) + */ +DO $DO$ +DECLARE + v_installed text := (SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'); + v_default text := (SELECT default_version FROM pg_available_extensions WHERE name = 'cat_tools'); +BEGIN + IF v_installed IS NULL THEN + RAISE EXCEPTION 'test_load_mode=existing but the cat_tools extension is not installed'; + END IF; + IF v_installed IS DISTINCT FROM v_default THEN + RAISE EXCEPTION + 'cat_tools is installed at version % but the current default_version is %' + , v_installed, v_default + ; + END IF; +END +$DO$; +\else +/* + * fresh / update: (re)install from scratch. Drop-first so a re-run on a + * persistent cluster installs the newest build instead of reusing stale + * objects. Drop the extension, then the roles. + * + * DROP EXTENSION does not remove cat_tools__usage: the extension scripts create + * it with CREATE ROLE, and roles are global objects, not extension members, so + * they survive DROP EXTENSION. The 0.2.2 install script uses a bare CREATE ROLE + * (unlike the current version's duplicate-tolerant DO block), so a leftover role + * would break a re-run in update mode. Drop all three roles explicitly via + * pg_temp.drop_role(): DROP OWNED BY first strips privileges granted TO the role + * (e.g. CREATE on public, cat_tools__usage membership) so DROP ROLE cannot fail + * with a dependency error; the pg_roles guard skips a not-yet-existing role + * (DROP OWNED BY errors on one), and format(%I) quotes the name correctly for + * the test roles (which contain spaces and mixed case, so they must be quoted). + */ +DROP EXTENSION IF EXISTS cat_tools CASCADE; + +CREATE FUNCTION pg_temp.drop_role( + role_name text +) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = role_name) THEN + EXECUTE format('DROP OWNED BY %I', role_name); + EXECUTE format('DROP ROLE IF EXISTS %I', role_name); + END IF; +END +$$; + +SELECT pg_temp.drop_role(:'use_role'); +SELECT pg_temp.drop_role(:'no_use_role'); +SELECT pg_temp.drop_role('cat_tools__usage'); + +\if :cat_tools_mode_update +/* + * update mode: install an older version, then ALTER EXTENSION UPDATE. The + * from/to versions come from the Makefile (TEST_UPDATE_FROM / TEST_UPDATE_TO, + * exported as GUCs). An empty test_update_to means "update to the current + * default_version" (the widest path); a non-empty value targets a specific + * version (e.g. the explicit 0.2.1 -> 0.2.2 script). + */ +SELECT current_setting('cat_tools.test_update_from') AS cat_tools_test_update_from \gset +SELECT current_setting('cat_tools.test_update_to') AS cat_tools_test_update_to \gset +/* + * Build the optional target clause once so a SINGLE ALTER EXTENSION covers both + * cases: an empty test_update_to yields '' (update to the current + * default_version -- the widest path); a non-empty value yields "TO ''". + * format(%L) quotes the version literal safely; the bare :clause interpolation + * below then drops it in verbatim. + */ +SELECT CASE WHEN :'cat_tools_test_update_to' = '' THEN '' + ELSE format('TO %L', :'cat_tools_test_update_to') END + AS cat_tools_update_to_clause \gset + +CREATE EXTENSION cat_tools VERSION :'cat_tools_test_update_from'; +/* + * Suppress the deprecation NOTICEs the update scripts emit, matching the + * approach used by test/build/upgrade.sql. + */ +SET client_min_messages = ERROR; +ALTER EXTENSION cat_tools UPDATE :cat_tools_update_to_clause; +SET client_min_messages = WARNING; +\else +CREATE EXTENSION cat_tools; +\endif +-- end \if :cat_tools_mode_update (fresh vs. update install branch) +\endif +-- end \if :cat_tools_mode_existing (existing mode skips the whole (re)install block) + +/* + * Roles and grants the test suite depends on. Formerly created per-test in + * deps.sql; now committed here once. Created idempotently because the source + * differs by mode: fresh/update dropped them just above, while a freshly + * pg_upgraded database (existing mode) never had them (roles are global, and + * only the extension -- not the test roles -- is created before the pg_upgrade). + * GRANT is idempotent, so re-applying the grants is always safe. + */ +CREATE FUNCTION pg_temp.create_role( + role_name text +) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = role_name) THEN + EXECUTE format('CREATE ROLE %I', role_name); + END IF; +END +$$; + +SELECT pg_temp.create_role(:'no_use_role'); +SELECT pg_temp.create_role(:'use_role'); + +GRANT cat_tools__usage TO :"use_role"; +/* + * PG15+ removed CREATE on the public schema from PUBLIC; grant it explicitly + * for tests that create shadow names in public to check catalog-lookup + * correctness. + */ +GRANT CREATE ON SCHEMA public TO :"use_role"; + +-- vi: expandtab ts=2 sw=2 diff --git a/test/load_new.sql b/test/load_new.sql deleted file mode 100644 index d87ae34..0000000 --- a/test/load_new.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Now load our extension. We don't use IF NOT EXISTs here because we want an - * error if the extension is already loaded (because we want to ensure we're - * getting the very latest version). - */ -CREATE EXTENSION cat_tools; diff --git a/test/load_upgrade.sql b/test/load_upgrade.sql deleted file mode 100644 index 5507205..0000000 --- a/test/load_upgrade.sql +++ /dev/null @@ -1,4 +0,0 @@ --- NOTE: To set this prior to 0.2.0 you need to hack the installed extension file to not create the cat_tools schema, or hack the current control file to not specify it! -CREATE EXTENSION cat_tools VERSION '0.2.0'; -ALTER EXTENSION cat_tools UPDATE; - diff --git a/test/roles.sql b/test/roles.sql new file mode 100644 index 0000000..f7a5199 --- /dev/null +++ b/test/roles.sql @@ -0,0 +1,21 @@ +/* + * Single source of truth for the test role names. + * + * The suite refers to two roles through psql variables. Each variable holds the + * RAW (unquoted) identifier; every use site is responsible for quoting it: + * - :"use_role" -> a quoted SQL identifier (SET ROLE, GRANT ... TO, ...) + * - :'use_role' -> the name as a string LITERAL (has_*_privilege(), etc.) + * - %I / quote_ident() in dynamic SQL + * + * The names deliberately contain spaces, a colon, and mixed case so they REQUIRE + * double-quoting: any site that interpolates the bare identifier (:use_role) + * would break the token apart / fold it to lower case and fail with + * "role ... does not exist", surfacing a missing-quote bug instead of silently + * passing. + * + * Both test/install/load.sql (which creates and drops the roles, committed once + * before the suite) and test/deps.sql (per test, because psql variables are + * session-local) \i this file so the names live in exactly one place. + */ +\set no_use_role 'cat_tools testing: NO USE' +\set use_role 'cat_tools testing: USE' diff --git a/test/setup.sql b/test/setup.sql index e0f6476..9097b62 100644 --- a/test/setup.sql +++ b/test/setup.sql @@ -1,7 +1,7 @@ -- Pulls in deps.sql \i test/pgxntool/setup.sql -GRANT USAGE ON SCHEMA tap TO :use_role, :no_use_role; +GRANT USAGE ON SCHEMA tap TO :"use_role", :"no_use_role"; CREATE FUNCTION pg_temp.exec( sql text @@ -33,4 +33,6 @@ SELECT array_to_string(array( ) $body$; + + -- vi: expandtab ts=2 sw=2 diff --git a/test/sql/attribute.sql b/test/sql/attribute.sql index b9ef1a7..6fadecc 100644 --- a/test/sql/attribute.sql +++ b/test/sql/attribute.sql @@ -18,7 +18,7 @@ SELECT plan( + 4 -- pg_attribute__get() ); -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( @@ -33,7 +33,7 @@ SELECT throws_ok( FROM func_calls ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; /* * pg_attribute__get() diff --git a/test/sql/enum.sql b/test/sql/enum.sql index c37efbc..8cc9c0c 100644 --- a/test/sql/enum.sql +++ b/test/sql/enum.sql @@ -17,7 +17,7 @@ SELECT lives_ok( , 'Create test enum' ); -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( 'SELECT cat_tools.enum_range%s( %L )', suffix, :'test_enum' ) , '42501' @@ -27,7 +27,7 @@ SELECT throws_ok( FROM unnest( array['', '_srf'] ) AS suffix ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; -- This will test both functions SELECT results_eq( diff --git a/test/sql/extension.sql b/test/sql/extension.sql index c6032a5..904132e 100644 --- a/test/sql/extension.sql +++ b/test/sql/extension.sql @@ -4,7 +4,7 @@ -- test_role is set in test/deps.sql -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; SELECT plan( 0 diff --git a/test/sql/function.sql b/test/sql/function.sql index 198ae72..e0b0f59 100644 --- a/test/sql/function.sql +++ b/test/sql/function.sql @@ -3,77 +3,67 @@ \i test/setup.sql \set s cat_tools -CREATE TEMP VIEW func_calls AS - SELECT * FROM (VALUES - ('function__arg_types'::name, $$'x'$$::text) - , ('regprocedure'::name, $$'x', 'x'$$) - ) v(fname, args) -; -GRANT SELECT ON func_calls TO public; +\set _s _cat_tools SELECT plan( - 0 - + (SELECT count(*)::int FROM func_calls) - - + 4 -- function__arg_types() - - + 2 -- regprocedure() + 2 -- security definer checks for _cat_tools helpers + + 1 -- regprocedure() + + 4 -- deprecated function__arg_types() wrappers ); -SET LOCAL ROLE :no_use_role; - -SELECT throws_ok( - format( - $$SELECT %I.%I( %L )$$ - , :'s', fname - , args - ) - , '42501' - , NULL - , 'Verify public has no perms' - ) - FROM func_calls -; - -SET LOCAL ROLE :use_role; +/* + * CRITICAL SECURITY TESTS: Helper functions must NOT be SECURITY DEFINER. + * If they were, they could be exploited for SQL injection since they execute + * dynamic SQL. + */ + +\set f function__arg_to_regprocedure +\set args_text 'text, text, text' +SELECT string_to_array(:'args_text', ', ') AS args \gset +SELECT isnt_definer(:'_s', :'f', :'args'::name[]); + +\set f function__drop_temp +\set args_text 'regprocedure, text' +SELECT string_to_array(:'args_text', ', ') AS args \gset +SELECT isnt_definer(:'_s', :'f', :'args'::name[]); + +/* + * Deprecated wrappers call through to routine__parse_arg_types, which has a + * security check that throws when current_user != session_user. SET SESSION + * AUTHORIZATION satisfies that check for the rest of this file. + */ +SET SESSION AUTHORIZATION :"use_role"; SELECT is( - :s.function__arg_types($$IN in_int int, INOUT inout_int_array int[], OUT out_char "char", anyelement, boolean DEFAULT false$$) - , '{int,int[],anyelement,boolean}'::regtype[] - , 'Verify function__arg_types() with INOUT and OUT' + :s.regprocedure('array_length', 'anyarray, integer') + , 'array_length(anyarray,integer)'::regprocedure + , 'Verify regprocedure()' ); +-- Test deprecated wrapper functions still work +\set VERBOSITY terse SELECT is( - :s.function__arg_types($$IN in_int int, INOUT inout_int_array int[], anyarray, anyelement, boolean DEFAULT false$$) - , '{int,int[],anyarray,anyelement,boolean}'::regtype[] - , 'Verify function__arg_types() with just INOUT' + :s.function__arg_types($$IN in_int int, INOUT inout_int_array int[], OUT out_char "char", anyelement, boolean DEFAULT false$$) + , '{int,int[],anyelement,boolean}'::regtype[] + , 'Verify deprecated function__arg_types() with INOUT and OUT' ); SELECT is( - :s.function__arg_types($$IN in_int int, OUT out_char "char", anyarray, anyelement, boolean DEFAULT false$$) - , '{int,anyarray,anyelement,boolean}'::regtype[] - , 'Verify function__arg_types() with just OUT' + :s.function__arg_types($$int, text$$) + , '{int,text}'::regtype[] + , 'Verify deprecated function__arg_types() with simple args' ); SELECT is( - :s.function__arg_types($$anyelement, "char", pg_class, VARIADIC boolean[]$$) - , '{anyelement,"\"char\"",pg_class,boolean[]}'::regtype[] - , 'Verify function__arg_types() with only inputs' -); - -\set args 'anyarray, OUT text, OUT "char", pg_class, int, VARIADIC boolean[]' -SELECT lives_ok( - format( - $$CREATE FUNCTION pg_temp.test_function(%s) LANGUAGE plpgsql AS $body$BEGIN NULL; END$body$;$$ - , :'args' - ) - , format('Create pg_temp.test_function(%s)', :'args') + :s.function__arg_types_text($$IN in_int int, INOUT inout_int_array int[], OUT out_char "char", anyelement, boolean DEFAULT false$$) + , 'integer, integer[], anyelement, boolean' + , 'Verify deprecated function__arg_types_text() with INOUT and OUT' ); SELECT is( - :s.regprocedure( 'pg_temp.test_function', :'args' ) - , 'pg_temp.test_function'::regproc::regprocedure - , 'Verify regprocedure()' + :s.function__arg_types_text($$int, text$$) + , 'integer, text' + , 'Verify deprecated function__arg_types_text() with simple args' ); \i test/pgxntool/finish.sql diff --git a/test/sql/name__check.sql b/test/sql/name__check.sql index 8a8e165..e65db56 100644 --- a/test/sql/name__check.sql +++ b/test/sql/name__check.sql @@ -7,7 +7,7 @@ SELECT plan(5); -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( @@ -20,7 +20,7 @@ SELECT throws_ok( , 'Verify public has no perms' ); -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; SELECT lives_ok( format( diff --git a/test/sql/object_type.sql b/test/sql/object_type.sql index e230e9e..822a89a 100644 --- a/test/sql/object_type.sql +++ b/test/sql/object_type.sql @@ -4,7 +4,7 @@ -- test_role is set in test/deps.sql -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; CREATE FUNCTION pg_temp.extra_types() RETURNS text[] LANGUAGE sql IMMUTABLE AS $$ @@ -72,11 +72,11 @@ SELECT is( ); SELECT is( (SELECT count(*)::int FROM obj_type) - , 51 + , 53 , 'sanity check size of pg_temp.obj_type' ); -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( 'SELECT NULL::%I', typename ) , '42704' -- undefined_object; not exactly correct, but close enough @@ -100,7 +100,7 @@ SELECT throws_ok( ) v(argtype) ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; SELECT function_returns( 'cat_tools' diff --git a/test/sql/permissions.sql b/test/sql/permissions.sql new file mode 100644 index 0000000..d26f592 --- /dev/null +++ b/test/sql/permissions.sql @@ -0,0 +1,102 @@ +\set ECHO none + +\i test/setup.sql + +/* + * Dynamically verify the permission model for the entire cat_tools public API: + * - no_use_role (no cat_tools__usage grant) cannot use any type or execute any function + * - use_role (has cat_tools__usage) can use all types and execute all functions + * + * Views are created as superuser so both roles can read them via explicit grants. + */ +CREATE TEMP VIEW cat_types AS +SELECT 'cat_tools.' || typname AS type_name + FROM pg_type t + WHERE t.typnamespace = 'cat_tools'::regnamespace + AND t.typtype IN ('e', 'c') -- enums and composite types + ORDER BY typname +; +GRANT SELECT ON cat_types TO :"use_role", :"no_use_role"; + +CREATE TEMP VIEW cat_functions AS +SELECT p.oid, p.proname, pg_get_function_arguments(p.oid) AS args + FROM pg_proc p + WHERE p.pronamespace = 'cat_tools'::regnamespace + ORDER BY proname, pg_get_function_arguments(p.oid) +; +GRANT SELECT ON cat_functions TO :"use_role", :"no_use_role"; + +SELECT plan( + (SELECT count(*)::int FROM cat_types) * 2 -- no_use denied + use allowed + + (SELECT count(*)::int FROM cat_functions) * 2 -- no_use denied + use allowed + + 2 -- _cat_tools schema access (no_use denied + use allowed) +); + +/* + * Function privilege checks via pg_catalog privilege functions. + * has_function_privilege(user, func, priv) checks the named role's privilege + * including inherited roles, independent of the current session role. + */ +SELECT is( + has_function_privilege(:'no_use_role', oid, 'EXECUTE') + , false + , format('Permission denied trying to execute cat_tools.%s(%s)', proname, args) + ) + FROM cat_functions +; + +SELECT is( + has_function_privilege(:'use_role', oid, 'EXECUTE') + , true + , format('Permission granted to execute cat_tools.%s(%s)', proname, args) + ) + FROM cat_functions +; + +/* + * Type access checks via role switching. + * Attempts actual casts to verify enforcement at the SQL level. + */ +SET LOCAL ROLE :"no_use_role"; + +SELECT throws_ok( + format('SELECT NULL::%s', type_name) + , '42501' + , NULL + , format('Permission denied trying to use type %s', type_name) + ) + FROM cat_types +; + +SET LOCAL ROLE :"use_role"; + +SELECT lives_ok( + format('SELECT NULL::%s', type_name) + , format('Permission granted to use type %s', type_name) + ) + FROM cat_types +; + +/* + * _cat_tools schema access checks. + * + * Schema-level USAGE is the primary barrier: no_use_role cannot resolve any + * object name in _cat_tools, making its functions effectively private. + * function__arg_to_regprocedure and function__drop_temp also have explicit + * REVOKE EXECUTE FROM PUBLIC for defense in depth. + */ +SELECT is( + has_schema_privilege(:'no_use_role', '_cat_tools', 'USAGE') + , false + , 'no_use_role has no USAGE on _cat_tools schema' +); + +SELECT is( + has_schema_privilege(:'use_role', '_cat_tools', 'USAGE') + , true + , 'use_role has USAGE on _cat_tools schema' +); + +\i test/pgxntool/finish.sql + +-- vi: expandtab ts=2 sw=2 diff --git a/test/sql/pg_depends.sql b/test/sql/pg_depends.sql index c0bfdfa..ff149f7 100644 --- a/test/sql/pg_depends.sql +++ b/test/sql/pg_depends.sql @@ -75,7 +75,7 @@ END; * END tests to run as owner */ -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT CASE WHEN pg_temp.major() < 903 THEN @@ -96,7 +96,7 @@ SELECT CASE FROM views ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; \i test/pgxntool/finish.sql diff --git a/test/sql/relation__.sql b/test/sql/relation__.sql new file mode 100644 index 0000000..3c3e558 --- /dev/null +++ b/test/sql/relation__.sql @@ -0,0 +1,219 @@ +\set ECHO none + +\i test/setup.sql + +\set s cat_tools + +SET LOCAL ROLE :"use_role"; + +/* + * Canonical pg_class.relkind -> cat_tools.relation_type mapping. + * + * These pairs are the ground truth per PostgreSQL's documented pg_class.relkind + * semantics (src/include/catalog/pg_class.h RELKIND_* constants), and were + * verified against the live catalog by creating one object of each kind and + * reading back pg_class.relkind. + * + * They are hard-coded here on purpose. A previous version of this test built + * `kinds` by positionally zipping enum_range('relation_type') against + * enum_range('relation_relkind') and then asserted the mapping functions agreed + * with that zip. Because the functions were written to the same positional + * pairing, the test was tautological: an internally-consistent-but-wrong + * mapping (e.g. c<->materialized view, f<->composite type, m<->foreign table + * swapped) still passed. Asserting against these literal pairs makes the test + * fail if the mapping is ever backwards. + */ +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') + , ('p', 'partitioned table') + , ('I', 'partitioned index') +; + +SELECT plan( + (1 + 2 + 2 * (SELECT count(*)::int FROM kinds)) -- relation_type enum mapping + + 5 -- relation__is_temp + + 4 -- relation__is_catalog + + 5 -- relation__column_names + + 1 -- relkind drift check vs pg_class.h +); + +-- relation_type enum mapping +SELECT is( + (SELECT count(*)::int FROM kinds) + , 10 + , 'Verify count from kinds' +); + +SELECT is( + cat_tools.relation__kind('r') + , 'table' + , 'Simple sanity check of relation__kind()' +); +SELECT is( + cat_tools.relation__relkind('table') + , 'r' + , 'Simple sanity check of relation__relkind()' +); + +SELECT is(cat_tools.relation__relkind(kind)::text, relkind, format('SELECT cat_tools.relation_relkind(%L)', kind)) + FROM kinds +; + +SELECT is(cat_tools.relation__kind(relkind)::text, kind, format('SELECT cat_tools.relation_type(%L)', relkind)) + FROM kinds +; + +/* + * Drift check: every relkind the PostgreSQL we are built against defines must + * appear in the hard-coded `kinds` data set above. test/gen-relkinds.sh (run by + * `make`) extracts every RELKIND_* value from the server's pg_class.h into + * pg_class_relkind_source; set_has() asserts `kinds` contains all of them. A + * relkind in pg_class.h but missing from `kinds` means PostgreSQL added (or + * renamed) one this extension does not handle yet -- fail so we notice and + * update the enum, the mapping functions, and `kinds`. + * + * set_has() is a subset check, not equality, which is deliberate: + * - `kinds` listing a relkind an older PostgreSQL lacks is allowed. + * - when the server headers are unavailable pg_class_relkind_source is empty + * (see gen-relkinds.sh) and the empty set is trivially contained, so + * `make test` passes identically with or without postgresql-server-dev-NN. + */ +\i test/.generated/pg_class_relkinds.sql + +SELECT set_has( + $$SELECT relkind FROM kinds$$ + , $$SELECT relkind FROM pg_class_relkind_source$$ + , 'cat_tools handles every relkind defined in pg_class.h' +); + +-- relation__is_temp +\set f relation__is_temp + +SET LOCAL ROLE :"no_use_role"; + +SELECT throws_ok( + format( + $$SELECT %I.%I( %L )$$ + , :'s', :'f' + , 'pg_catalog.pg_class' + ) + , '42501' + , NULL + , 'Verify public has no perms' +); + +SET LOCAL ROLE :"use_role"; + +SELECT is( + cat_tools.relation__is_temp('pg_catalog.pg_class'::regclass) + , false + , 'pg_catalog.pg_class is not a temp relation' +); + +/* + * One temp table is shared by the relation__is_temp, relation__is_catalog and + * relation__column_names tests below: a temp relation (hence not in pg_catalog) + * with a known set of columns is everything those three functions need. + */ +SELECT lives_ok( + $$CREATE TEMP TABLE rel_test(col1 int, col2 text, col3 boolean, col4 timestamp, col5 numeric)$$ + , 'Create shared temp table for is_temp/is_catalog/column_names tests' +); + +SELECT is( + cat_tools.relation__is_temp('rel_test'::regclass) + , true + , 'temp relation is correctly identified as temp' +); + +SELECT is( + cat_tools.relation__is_temp(NULL) + , NULL + , 'NULL input returns NULL (STRICT function)' +); + +-- relation__is_catalog +\set f relation__is_catalog + +SET LOCAL ROLE :"no_use_role"; + +SELECT throws_ok( + format( + $$SELECT %I.%I( %L )$$ + , :'s', :'f' + , 'pg_catalog.pg_class' + ) + , '42501' + , NULL + , 'Verify public has no perms' +); + +SET LOCAL ROLE :"use_role"; + +SELECT is( + cat_tools.relation__is_catalog('pg_catalog.pg_class'::regclass) + , true + , 'pg_catalog.pg_class is in pg_catalog schema' +); + +SELECT is( + cat_tools.relation__is_catalog('rel_test'::regclass) + , false + , 'temp relation is not in pg_catalog schema' +); + +SELECT is( + cat_tools.relation__is_catalog(NULL) + , NULL + , 'NULL input returns NULL (STRICT function)' +); + +-- relation__column_names +\set f relation__column_names + +SET LOCAL ROLE :"no_use_role"; + +SELECT throws_ok( + format( + $$SELECT %I.%I( %L )$$ + , :'s', :'f' + , 'rel_test' + ) + , '42501' + , NULL + , 'Verify public has no perms' +); + +SET LOCAL ROLE :"use_role"; + +SELECT is( + cat_tools.relation__column_names('rel_test'::regclass) + , '{col1,col2,col3,col4,col5}'::text[] + , 'Temp table returns expected column names' +); + +SELECT lives_ok($$ALTER TABLE rel_test DROP COLUMN col3$$, 'Drop middle column from temp table'); + +SELECT is( + cat_tools.relation__column_names('rel_test'::regclass) + , '{col1,col2,col4,col5}'::text[] + , 'Temp table with dropped column returns expected column names' +); + +SELECT is( + cat_tools.relation__column_names(NULL) + , NULL + , 'NULL input returns NULL (STRICT function)' +); + +\i test/pgxntool/finish.sql + +-- vi: expandtab ts=2 sw=2 diff --git a/test/sql/relation_type.sql b/test/sql/relation_type.sql deleted file mode 100644 index 69beaf1..0000000 --- a/test/sql/relation_type.sql +++ /dev/null @@ -1,85 +0,0 @@ -\set ECHO none - -\i test/setup.sql - --- 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 -; - -SELECT plan( - 1 - + 2 -- Simple is() tests - + 4 -- no_use tests - + 3 * (SELECT count(*)::int FROM kinds) -); - -SELECT is( - (SELECT count(*)::int FROM kinds) - , 8 - , 'Verify count from kinds' -); - -SELECT is( - cat_tools.relation__kind('r') - , 'table'::cat_tools.relation_type - , 'Simple sanity check of relation__kind()' -); -SELECT is( - cat_tools.relation__relkind('table') - , 'r'::cat_tools.relation_relkind - , 'Simple sanity check of relation__relkind()' -); - -SET LOCAL ROLE :no_use_role; -SELECT throws_ok( - format( 'SELECT NULL::%I', typename ) - , '42704' -- undefined_object; not exactly correct, but close enough - , NULL - , 'Permission denied trying to use types' -) - FROM (VALUES - ('cat_tools.relation__relkind') - , ('cat_tools.relation__kind') - ) v(typename) -; -SELECT throws_ok( - format( 'SELECT cat_tools.relation__%s( NULL::%I )', suffix, argtype ) - , '42501' -- insufficient_privilege - , NULL - , 'Permission denied trying to run functions' -) - FROM (VALUES - ('kind', 'text'::regtype) - , ('relkind', 'text'::regtype) - ) v(suffix, argtype) -; - -SET LOCAL ROLE :use_role; - -SELECT is(cat_tools.relation__relkind(kind)::text, relkind, format('SELECT cat_tools.relation_relkind(%L)', kind)) - FROM kinds -; - -SELECT is(cat_tools.relation__kind(relkind)::text, kind, format('SELECT cat_tools.relation_type(%L)', relkind)) - FROM kinds -; - -SELECT is(cat_tools.relation__kind(relkind::"char")::text, kind, format('SELECT cat_tools.relation_type(%L::"char")', relkind)) - FROM kinds -; - -\i test/pgxntool/finish.sql - --- vi: expandtab ts=2 sw=2 diff --git a/test/sql/routine__.sql b/test/sql/routine__.sql new file mode 100644 index 0000000..5b67b93 --- /dev/null +++ b/test/sql/routine__.sql @@ -0,0 +1,292 @@ +\set ECHO none + +\i test/setup.sql + +\set s cat_tools + +SET LOCAL ROLE :"use_role"; + +SELECT plan( + (1 + 2 + 3 * array_length(cat_tools.enum_range('cat_tools.routine_type'), 1)) + + (1 + 2 + 3 * array_length(cat_tools.enum_range('cat_tools.routine_argument_mode'), 1)) + + (1 + 2 + 3 * array_length(cat_tools.enum_range('cat_tools.routine_volatility'), 1)) + + (1 + 2 + 3 * array_length(cat_tools.enum_range('cat_tools.routine_parallel_safety'), 1)) + + 2 -- no_use_role access denied for parse helpers (throws_ok via func_calls) + + 1 -- security check when current_user != session_user + + 1 -- create pg_temp.test_function + + 1 -- create pg_temp.named_function + + 4 -- routine__parse_arg_types() tests + + 1 -- isnt_definer: routine__parse_arg_types + + 1 -- isnt_definer: routine__parse_arg_types_text + + 4 -- routine__arg_types() tests + + 4 -- routine__arg_types_text() tests + + 4 -- routine__parse_arg_names() tests + + 1 -- isnt_definer: routine__parse_arg_names + + 1 -- isnt_definer: routine__parse_arg_names_text + + 3 -- routine__arg_names() tests + + 4 -- routine__arg_names_text() tests +); + +\set kind type +\set char_col prokind +\set sample_char f +\set sample_text function +\i test/helpers/enum_mapping.sql + +\set kind argument_mode +\set char_col proargmode +\set sample_char i +\set sample_text in +\i test/helpers/enum_mapping.sql + +\set kind volatility +\set char_col provolatile +\set sample_char i +\set sample_text immutable +\i test/helpers/enum_mapping.sql + +\set kind parallel_safety +\set char_col proparallel +\set sample_char s +\set sample_text safe +\i test/helpers/enum_mapping.sql + +CREATE TEMP VIEW func_calls AS + SELECT * FROM (VALUES + ('routine__parse_arg_types'::name, $$'x'$$::text) + , ('routine__parse_arg_names'::name, $$'x'$$::text) + ) v(fname, args) +; +GRANT SELECT ON func_calls TO public; + +SET LOCAL ROLE :"no_use_role"; + +SELECT throws_ok( + format( + $$SELECT %I.%I( %L )$$ + , :'s', fname + , args + ) + , '42501' + , NULL + , 'Verify public has no perms' + ) + FROM func_calls +; + +/* + * Test that the security check works when current_user != session_user. + * SET LOCAL ROLE makes current_user = use_role while session_user = superuser, + * so current_user != session_user, and the function should throw 28000. + */ +SET LOCAL ROLE :"use_role"; +SELECT throws_ok( + $$SELECT cat_tools.routine__parse_arg_types('int')$$ + , '28000' + , 'potential use of SECURITY DEFINER detected' + , 'Security check should prevent execution when current_user != session_user' +); + +/* + * SET SESSION AUTHORIZATION satisfies the current_user = session_user check + * required by the parse helper security guard. All functional tests below + * run under this authorization. + */ +SET SESSION AUTHORIZATION :"use_role"; + +/* + * routine__parse_arg_types / routine__parse_arg_types_text + */ + +SELECT is( + :s.routine__parse_arg_types($$IN in_int int, INOUT inout_int_array int[], OUT out_char "char", anyelement, boolean DEFAULT false$$) + , '{int,int[],anyelement,boolean}'::regtype[] + , 'Verify routine__parse_arg_types() with INOUT and OUT' +); + +SELECT is( + :s.routine__parse_arg_types($$IN in_int int, INOUT inout_int_array int[], anyarray, anyelement, boolean DEFAULT false$$) + , '{int,int[],anyarray,anyelement,boolean}'::regtype[] + , 'Verify routine__parse_arg_types() with just INOUT' +); + +SELECT is( + :s.routine__parse_arg_types($$IN in_int int, OUT out_char "char", anyarray, anyelement, boolean DEFAULT false$$) + , '{int,anyarray,anyelement,boolean}'::regtype[] + , 'Verify routine__parse_arg_types() with just OUT' +); + +SELECT is( + :s.routine__parse_arg_types($$anyelement, "char", pg_class, VARIADIC boolean[]$$) + , '{anyelement,"\"char\"",pg_class,boolean[]}'::regtype[] + , 'Verify routine__parse_arg_types() with only inputs' +); + +/* + * CRITICAL SECURITY TESTS: public routine__ functions must NOT be SECURITY DEFINER. + * If they were, they could be exploited for SQL injection since they execute + * dynamic SQL with elevated privileges. + */ + +\set f routine__parse_arg_types +SELECT string_to_array('text', ', ') AS _f_args \gset +SELECT isnt_definer(:'s', :'f', :'_f_args'::name[]); + +\set f routine__parse_arg_types_text +SELECT isnt_definer(:'s', :'f', :'_f_args'::name[]); + +-- Create pg_temp.test_function now that we have a stable session_user. +\set args 'anyarray, OUT text, OUT "char", pg_class, int, VARIADIC boolean[]' +SELECT lives_ok( + format( + $$CREATE FUNCTION pg_temp.test_function(%s) LANGUAGE plpgsql AS $body$BEGIN NULL; END$body$;$$ + , :'args' + ) + , format('Create pg_temp.test_function(%s)', :'args') +); + +/* + * routine__arg_types / routine__arg_types_text + */ + +SELECT is( + :s.routine__arg_types(:s.regprocedure('pg_temp.test_function', :'args')) + , '{anyarray,pg_class,integer,boolean[]}'::regtype[] + , 'Verify routine__arg_types() returns all argument types' +); + +SELECT is( + :s.routine__arg_types('array_length(anyarray,integer)'::regprocedure) + , '{anyarray,integer}'::regtype[] + , 'Verify routine__arg_types() with IN arguments only' +); + +SELECT is( + :s.routine__arg_types('pg_backend_pid()'::regprocedure) + , '{}'::regtype[] + , 'Verify routine__arg_types() with no arguments' +); + +SELECT is( + :s.routine__arg_types('concat("any")'::regprocedure) + , '{"\"any\""}'::regtype[] + , 'Verify routine__arg_types() with VARIADIC argument' +); + +SELECT is( + :s.routine__arg_types_text(:s.regprocedure('pg_temp.test_function', :'args')) + , 'anyarray, pg_class, integer, boolean[]' + , 'Verify routine__arg_types_text() formatting' +); + +SELECT is( + :s.routine__arg_types_text('array_length(anyarray,integer)'::regprocedure) + , 'anyarray, integer' + , 'Verify routine__arg_types_text() with simple types' +); + +SELECT is( + :s.routine__arg_types_text('pg_backend_pid()'::regprocedure) + , '' + , 'Verify routine__arg_types_text() with no arguments' +); + +SELECT is( + :s.routine__arg_types_text('concat("any")'::regprocedure) + , '"any"' + , 'Verify routine__arg_types_text() with VARIADIC' +); + +/* + * routine__parse_arg_names / routine__parse_arg_names_text + */ + +SELECT is( + :s.routine__parse_arg_names($$IN in_int int, INOUT inout_int_array int[], OUT out_char "char", anyelement, boolean DEFAULT false$$) + , '{in_int,inout_int_array,NULL,NULL}'::text[] + , 'Verify routine__parse_arg_names() with INOUT and OUT' +); + +SELECT is( + :s.routine__parse_arg_names($$IN in_int int, INOUT inout_int_array int[], anyarray, anyelement, boolean DEFAULT false$$) + , '{in_int,inout_int_array,NULL,NULL,NULL}'::text[] + , 'Verify routine__parse_arg_names() with just INOUT' +); + +SELECT is( + :s.routine__parse_arg_names($$IN in_int int, OUT out_char "char", anyarray, anyelement, boolean DEFAULT false$$) + , '{in_int,NULL,NULL,NULL}'::text[] + , 'Verify routine__parse_arg_names() with just OUT' +); + +SELECT is( + :s.routine__parse_arg_names($$anyelement, "char", pg_class, VARIADIC boolean[]$$) + , '{NULL,NULL,NULL,NULL}'::text[] + , 'Verify routine__parse_arg_names() with only inputs' +); + +\set f routine__parse_arg_names +SELECT isnt_definer(:'s', :'f', :'_f_args'::name[]); + +\set f routine__parse_arg_names_text +SELECT isnt_definer(:'s', :'f', :'_f_args'::name[]); + +/* + * routine__arg_names / routine__arg_names_text + */ + +-- Create pg_temp.named_function just before it is first used below. +\set named_args 'input_val int, INOUT inout_val text, OUT output_val boolean' +SELECT lives_ok( + format( + $$CREATE FUNCTION pg_temp.named_function(%s) LANGUAGE plpgsql AS $body$BEGIN output_val := true; END$body$;$$ + , :'named_args' + ) + , format('Create pg_temp.named_function(%s)', :'named_args') +); + +SELECT is( + :s.routine__arg_names(:s.regprocedure('pg_temp.test_function', :'args')) + , '{NULL,NULL,NULL,NULL}'::text[] + , 'Verify routine__arg_names() returns argument names (unnamed function)' +); + +SELECT is( + :s.routine__arg_names(:s.regprocedure('pg_temp.named_function', :'named_args')) + , '{input_val,inout_val}'::text[] + , 'Verify routine__arg_names() with named arguments' +); + +SELECT is( + :s.routine__arg_names('pg_backend_pid()'::regprocedure) + , '{}'::text[] + , 'Verify routine__arg_names() with no arguments' +); + +SELECT is( + :s.routine__arg_names_text(:s.regprocedure('pg_temp.named_function', :'named_args')) + , 'input_val, inout_val' + , 'Verify routine__arg_names_text() formatting' +); + +SELECT is( + :s.routine__arg_names_text(:s.regprocedure('pg_temp.test_function', :'args')) + , '' + , 'Verify routine__arg_names_text() with unnamed arguments' +); + +SELECT is( + :s.routine__arg_names_text('array_length(anyarray,integer)'::regprocedure) + , '' + , 'Verify routine__arg_names_text() with built-in function' +); + +SELECT is( + :s.routine__arg_names_text('pg_backend_pid()'::regprocedure) + , '' + , 'Verify routine__arg_names_text() with no arguments' +); + +\i test/pgxntool/finish.sql + +-- vi: expandtab ts=2 sw=2 diff --git a/test/sql/sequence.sql b/test/sql/sequence.sql index 61071e3..a408b32 100644 --- a/test/sql/sequence.sql +++ b/test/sql/sequence.sql @@ -4,7 +4,7 @@ -- test_role is set in test/deps.sql -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; CREATE TEMP TABLE seqtest(s serial); CREATE TEMP TABLE functions(fname name); @@ -29,7 +29,7 @@ SELECT plan(( )::int); GRANT SELECT ON functions TO public; -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( $$SELECT cat_tools.%I('seqtest', 's')$$, fname ) , '42501' -- insufficient_privilege @@ -39,7 +39,7 @@ SELECT throws_ok( FROM functions ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; SELECT function_returns( sname, fname, 'bigint'::regtype::text ) FROM fv; SELECT volatility_is( sname, fname, 'volatile' ) FROM fv; diff --git a/test/sql/trigger.sql b/test/sql/trigger.sql index 26785f9..76a3f49 100644 --- a/test/sql/trigger.sql +++ b/test/sql/trigger.sql @@ -19,7 +19,7 @@ SELECT plan( + 1 -- verify trigger__args_as_array() ); -SET LOCAL ROLE :no_use_role; +SET LOCAL ROLE :"no_use_role"; SELECT throws_ok( format( @@ -34,7 +34,7 @@ SELECT throws_ok( FROM unnest(:function_array) f ; -SET LOCAL ROLE :use_role; +SET LOCAL ROLE :"use_role"; CREATE TEMP TABLE "test table"(); diff --git a/test/sql/zzz_build.sql b/test/sql/zzz_build.sql deleted file mode 100644 index e9a7f60..0000000 --- a/test/sql/zzz_build.sql +++ /dev/null @@ -1,13 +0,0 @@ -\set ECHO none - -\i test/pgxntool/psql.sql -\t - -BEGIN; -CREATE SCHEMA cat_tools; - -\i sql/cat_tools.sql - -\echo # TRANSACTION INTENTIONALLY LEFT OPEN! - --- vi: expandtab sw=2 ts=2