diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec1f57d..4fd68fe 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,23 +25,26 @@ 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 pg-upgrade-test: strategy: matrix: include: - - 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_pg: "12" new_pg: "13" - old_pg: "12" @@ -68,6 +71,11 @@ jobs: pg_isready -t 30 - name: Check out the repo uses: actions/checkout@v6 + - name: Set expected extension version + run: | + EXPECTED_VERSION="$(sed -n "s/^default_version = '\(.*\)'$/\1/p" cat_tools.control)" + test -n "$EXPECTED_VERSION" || { echo "Failed to parse default_version from cat_tools.control"; exit 1; } + echo "EXPECTED_VERSION=$EXPECTED_VERSION" >> "$GITHUB_ENV" - name: Install rsync run: apt-get install -y rsync - name: Install cat_tools into old cluster @@ -104,9 +112,13 @@ jobs: run: | VERSION=$(psql -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") echo "Extension version: ${VERSION:-}" - echo "$VERSION" | grep -q "0.2.2" + test -n "$VERSION" || { echo "cat_tools extension version not found after upgrade"; exit 1; } + [ "$VERSION" = "$EXPECTED_VERSION" ] - name: Run test suite on upgraded cluster - run: make test + run: | + make test + # See note in the "test" job: make test does not fail on regressions. + make verify-results # 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 @@ -115,17 +127,17 @@ jobs: 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 upgrade path from 0.2.2 onwards. - pg: [10] + # Exercise the WIDEST update path we can on every supported version: + # install the oldest cat_tools version that still installs on the + # supported PostgreSQL range and update to the CURRENT version. 0.2.2 is + # that floor purely for backward-compat reasons β€” the 0.2.0/0.2.1 (and + # earlier) install scripts fail on PG11+/PG12+, so they cannot be the + # starting point here. PG12 is the PostgreSQL floor: the update runs + # `ALTER TYPE ... ADD VALUE`, which PG11 and below cannot run inside an + # extension update script (the restriction was lifted in PG12). There is + # no upper bound. (PG10/11 are dropped; the 0.2.0/0.2.1 update paths + # required them and can no longer be tested.) + pg: [18, 17, 16, 15, 14, 13, 12] name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -134,27 +146,31 @@ 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 + # server-dev provides catalog/pg_class.h for the relkind drift check + # (see the "test" job); required so check-relkind-source below passes. + run: apt-get install -y rsync postgresql-server-dev-${{ matrix.pg }} - name: Install cat_tools (all versions) run: make install - - name: Test upgrade from 0.2.0 (direct 0.2.0β†’0.2.2 path) - run: | - psql -c "CREATE EXTENSION cat_tools VERSION '0.2.0'" - psql -c "ALTER EXTENSION cat_tools UPDATE" - VERSION=$(psql -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") - echo "Version after 0.2.0 upgrade: ${VERSION:-}" - echo "$VERSION" | grep -q "0.2.2" - - name: Test upgrade from 0.2.1 (0.2.1β†’0.2.2 path) + - name: Run test suite against the upgraded (oldestβ†’current) database + # TEST_LOAD_SOURCE=upgrade makes the pgTAP suite load the extension via + # CREATE EXTENSION VERSION '0.2.2' + ALTER EXTENSION UPDATE to the + # current version (committed, before the suite, by test/install/load.sql) + # instead of a fresh CREATE EXTENSION. Running the SAME suite with the + # SAME expected output asserts an upgraded database behaves identically + # to a fresh install; a broken or incomplete update makes the suite fail, + # so this subsumes the old inline createdb/CREATE/ALTER/version-check + # steps. The committed upgrade is required because the update to the + # current version uses ALTER TYPE ... ADD VALUE, whose new values cannot + # be used in the same transaction (see test/install/load.sql). `make + # install` (all versions) already ran above, so the update scripts exist. + env: + TEST_LOAD_SOURCE: upgrade run: | - 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" - VERSION=$(psql -d cat_tools_from_021 -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") - echo "Version after 0.2.1 upgrade: ${VERSION:-}" - echo "$VERSION" | grep -q "0.2.2" - - name: Run test suite on updated extension - run: make test + make check-relkind-source + make test + # See note in the "test" job: make test does not fail on regressions. + make verify-results # 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 44f3caa..2defe38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,9 @@ ## GitHub CI -After pushing to a branch with an open PR, monitor CI using `gh pr checks --watch` in a background subagent until all jobs pass or a failure is confirmed. Investigate and fix failures immediately rather than leaving them for the user to notice. +After **every** push, monitor GitHub CI in a background subagent until all jobs pass or a failure is confirmed. Use `gh pr checks --watch` when the branch has an open PR; otherwise (a branch with no PR yet, or a push to `master`) use `gh run watch` for the pushed commit. Investigate and fix failures immediately rather than leaving them for the user to notice. + +(`paths-ignore` in `.github/workflows/ci.yml` skips CI only when the *entire* change set is docs-only β€” e.g. a docs-only push to `master`, or a PR whose whole diff is `**.md`/`**.asc`. A docs-only commit on a PR that also touches code still triggers CI on the full PR diff. When unsure, check `gh run list` for the pushed commit and monitor whatever run appears; if none does, there is nothing to watch.) ## Bug Fixes @@ -14,6 +16,13 @@ When fixing a bug, add a comment at the fix site explaining what the bug was and **Always open PRs against the main repo** (`Postgres-Extensions/cat_tools`), not a fork. +## Terminology + +- **Extension update**: moving from one cat_tools version to another (e.g. `ALTER EXTENSION cat_tools UPDATE`). Always say "update" for this. +- **PostgreSQL upgrade**: upgrading a PostgreSQL cluster to a newer major version (e.g. `pg_upgrade`, `pg_upgradecluster`). Always say "upgrade" for this. + +Never use "upgrade" to describe an extension version change, and never use "update" to describe a PostgreSQL cluster version change. + ## SQL file conventions Rules for what to track in git: @@ -21,26 +30,33 @@ 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. Upgrade scripts (e.g. `sql/cat_tools--0.2.1--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). 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 -script may support more PG versions, enabling testing of the upgrade path from older -cat_tools versions on newer PostgreSQL. +## 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+. + +The `extension-update-test` job exercises the widest update path we can β€” install the +oldest cat_tools version that still installs on the supported PostgreSQL range and update +to the current version β€” on the full `pg: [12..18]` matrix. It runs the pgTAP suite in +upgrade mode (`make test TEST_LOAD_SOURCE=upgrade`, backed by the committed-once +`test/install/load.sql`), so a broken or incomplete update makes the suite fail. `0.2.2` is +the starting floor only for backward-compat: the 0.2.0/0.2.1 (and earlier) install scripts +fail on PG11+/PG12+, so they cannot be the starting point. PG12 is the PostgreSQL floor β€” +PG11 (and PG10) cannot run `ALTER TYPE ... ADD VALUE` in extension update scripts; this +restriction was lifted in PG12. There is no upper bound. ## Code Style diff --git a/HISTORY.asc b/HISTORY.asc index b25e350..d8ed89f 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,7 +1,39 @@ +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.2β†’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.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 +47,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 +88,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 +106,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 +146,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/LICENSE b/LICENSE index de25054..f1e6077 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Jim Nasby +Copyright (c) 2016-2026 Jim Nasby Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/META.in.json b/META.in.json index 6f07609..e6de70b 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 2bd6367..fc6a9e6 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 2a551ee..c0ac65b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,58 @@ B = sql testdeps: $(wildcard test/*.sql test/helpers/*.sql) # Be careful not to include directories in this +# Committed-once install of the extension + test roles. +# +# 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). +# - upgrade: CREATE EXTENSION at the 0.2.2 backward-compat floor and +# ALTER EXTENSION UPDATE to the current version. Running the SAME suite +# with the SAME expected output against the upgraded database verifies it +# behaves identically to a fresh install. +# +# The mode is signalled to load.sql by the cat_tools.test_load_mode placeholder +# GUC. pg_regress does not forward make variables, but the psql processes it +# spawns inherit the environment, so PGOPTIONS reaches load.sql. +# +# The GUC is exported UNCONDITIONALLY (with the mode value), so load.sql can read +# it WITHOUT missing_ok and fail loudly if it 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 fresh in place of the +# intended upgrade test. Making the mode explicit and required removes that trap. +# +# TEST_LOAD_SOURCE must be exactly `fresh` or `upgrade`; anything else is a hard +# error at parse time (so e.g. `make test TEST_LOAD_SOURCE=typo` fails fast +# rather than defaulting). +# +# The upgrade 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. +# +# Upgrade mode requires PG12+ (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 upgrade),) +$(error TEST_LOAD_SOURCE must be 'fresh' or 'upgrade', got '$(TEST_LOAD_SOURCE)') +endif +export PGOPTIONS := $(PGOPTIONS) -c cat_tools.test_load_mode=$(TEST_LOAD_SOURCE) + +# Convenience wrapper: `make test-update` == `make test TEST_LOAD_SOURCE=upgrade`. +# Must recurse (a fresh $(MAKE)) rather than depend on `test`, so the parse-time +# TEST_LOAD_SOURCE conditional above re-evaluates with upgrade set. +.PHONY: test-update +test-update: + $(MAKE) test TEST_LOAD_SOURCE=upgrade + include pgxntool/base.mk LT95 = $(call test, $(MAJORVER), -lt, 95) @@ -17,16 +69,91 @@ upgrade_scripts_out = $(subst sql/,$B/,$(subst .sql.in,.sql,$(wildcard sql/*--*- # Pre-built historical install scripts (no .sql.in source available) DATA += sql/cat_tools--0.1.0.sql sql/cat_tools--0.1.3.sql sql/cat_tools--0.1.4.sql sql/cat_tools--0.1.5.sql -# Generated historical install scripts (built from .sql.in source). +# Generated install scripts (built from .sql.in source). # Exclude EXTENSION_VERSION_FILES (managed by control.mk) and upgrade scripts # ($(upgrade_scripts_out), already handled by base.mk) to avoid duplicates. DATA += $(filter-out $(EXTENSION_VERSION_FILES) $(upgrade_scripts_out), $(versioned_out)) all: $B/cat_tools.sql $(versioned_out) installcheck: $B/cat_tools.sql $(versioned_out) -EXTRA_CLEAN += $B/cat_tools.sql $(filter-out $(EXTENSION_VERSION_FILES), $(versioned_out)) -# Also clean the generated .sql.in for the current version -EXTRA_CLEAN += $(EXTENSION_VERSION_FILES:.sql=.sql.in) +EXTRA_CLEAN += $B/cat_tools.sql $(versioned_out) + +# Clean the cruft pg_regress writes into test/install/ (the self-comparing +# result .out and its diff), which is listed in test/install/.gitignore. +EXTRA_CLEAN += $(addprefix test/install/,$(shell grep -v '^\#' test/install/.gitignore 2>/dev/null)) + +# 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. +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 # Temporary ugly hack for 9.x β€” remove these two blocks when 9.x support is dropped. # $@ is deferred via = and expands to the target name at recipe time. @@ -88,42 +215,6 @@ $(EXTENSION_VERSION_FILES): $(EXTENSION_VERSION_FILES:.sql=.sql.in) pgxntool/saf $(_apply_version_seds) mv $@.tmp $@ -# Support for upgrade test -# -# 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_BUILD_DIR)/active.sql: $(TEST_BUILD_DIR)/dep.mk $(TEST_BUILD_DIR)/$(TEST_LOAD_SOURCE).sql - ln -sf $(TEST_LOAD_SOURCE).sql $@ - -$(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 $@ - -$(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 $@ - -# TODO: figure out vpath -EXTRA_CLEAN += $(TEST_BUILD_DIR)/ -$(TEST_BUILD_DIR): - [ -d $@ ] || mkdir -p $@ .PHONY: old_version old_version: $(DESTDIR)$(datadir)/extension/cat_tools--0.2.0.sql diff --git a/README.asc b/README.asc index 6f67413..a3163f9 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! @@ -68,4 +121,4 @@ Copyright and License Cat Tools is released under a https://github.com/decibel/cattools/blob/master/LICENSE[MIT license]. -Copyright (c) 2016 Jim Nasby . +Copyright (c) 2026 Jim Nasby . 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/pgxntool/.gitattributes b/pgxntool/.gitattributes index a94d824..8dc1599 100644 --- a/pgxntool/.gitattributes +++ b/pgxntool/.gitattributes @@ -1,5 +1,6 @@ .gitattributes export-ignore .claude/ export-ignore +.github/ export-ignore *.md export-ignore .DS_Store export-ignore *.asc export-ignore diff --git a/pgxntool/CLAUDE.md b/pgxntool/CLAUDE.md index 9d4c24b..75c9d16 100644 --- a/pgxntool/CLAUDE.md +++ b/pgxntool/CLAUDE.md @@ -2,12 +2,33 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## CI Monitoring After Every Push + +**REQUIRED**: After every `git push`, immediately start a background task to +monitor the CI run for that push. If you pushed to both pgxntool and +pgxntool-test, start a background task for each repo β€” do not monitor them +sequentially. + +The CI monitor lives in the pgxntool-test checkout: run +`bash ../pgxntool-test/.claude/skills/ci/scripts/monitor-ci.sh` (the `/ci` +skill). It monitors both repos and derives the owner from the current repo. +Pass the exact push SHA when available β€” `gh run list --branch` has a race +condition: if two pushes land close together on the same branch, `--branch` +may pick up the wrong run. `--commit SHA` targets the exact push and avoids it. + ## Scope of This File -The guidance in this CLAUDE.md applies to pgxntool's own source files and provides -recommended best practices for projects using pgxntool. However, any agent working in -an extension project should always defer to that project's own CLAUDE.md and -instructions over anything stated here. +**CLAUDE.md is for people USING pgxntool** β€” extension developers who have embedded +pgxntool into their project via `git subtree`. It documents the build system, available +commands, and how pgxntool works. + +**If you are making changes to pgxntool itself**, stop β€” you are in the wrong place. +See `.claude/` in this directory for developer guidelines. More importantly, pgxntool +development must be done from the **pgxntool-test** repository, not from here. See the +`Development Workflow` section below. + +Any agent working in an extension project should always defer to that project's own +CLAUDE.md and instructions over anything stated here. ## Git Commit Guidelines diff --git a/pgxntool/HISTORY.asc b/pgxntool/HISTORY.asc index a3e912b..b0894da 100644 --- a/pgxntool/HISTORY.asc +++ b/pgxntool/HISTORY.asc @@ -1,3 +1,21 @@ +2.1.0 +----- +== Fix setup.sh / pgxntool-sync.sh / update-setup-files.sh inside a git worktree +These scripts guarded the project root with `[ -d .git ]`. In a linked worktree +`.git` is a file, not a directory, so the check failed: setup.sh would wrongly +re-run `git init`, and the sync scripts aborted with "Not in a git repository." +They now detect the repo with `git rev-parse --git-dir`, which works in both a +normal clone and a worktree. + +== Fix `verify-results` checking stale results in `make results` +`make results` ran `verify-results` before `make test`, so it checked stale +`regression.diffs` from a prior run. Reordered so `verify-results` always +checks the fresh results. + +== Fix `PGXNTOOL_ENABLE_TEST_BUILD`/`PGXNTOOL_ENABLE_TEST_INSTALL` ignoring command-line values +Without `override`, the `pgxntool_validate_yesno` normalization was silently +skipped when these variables were set on the command line. + 2.0.3 ----- == Fix pgxntool-sync remote, and make it runnable without make diff --git a/pgxntool/README.asc b/pgxntool/README.asc index 03b9acc..db6162f 100644 --- a/pgxntool/README.asc +++ b/pgxntool/README.asc @@ -25,7 +25,9 @@ TODO: Create a nice script that will init a new project for you. == Development -If you want to contribute to pgxntool development, work from the https://github.com/decibel/pgxntool-test[pgxntool-test] repository, not from this repository. That repository contains the test infrastructure and development tools needed to validate changes to pgxntool. This repository contains only the framework files that get embedded into extension projects via `git subtree`. +If you want to contribute to pgxntool development, work from the https://github.com/Postgres-Extensions/pgxntool-test[pgxntool-test] repository, not from this repository. That repository contains the test infrastructure and development tools needed to validate changes to pgxntool. This repository contains only the framework files that get embedded into extension projects via `git subtree`. + +Changes are normally paired across both repos: a `pgxntool` change should come with a matching branch (same name, on the same account) and PR in `pgxntool-test`, and CI enforces this pairing. See the https://github.com/Postgres-Extensions/pgxntool-test#ci-and-contributing[CI and Contributing] section in pgxntool-test for the full workflow. == Usage Typically, you can just create a simple Makefile that does nothing but include base.mk: diff --git a/pgxntool/README.html b/pgxntool/README.html index a46c633..431358f 100644 --- a/pgxntool/README.html +++ b/pgxntool/README.html @@ -542,7 +542,10 @@

2. Development

-

If you want to contribute to pgxntool development, work from the pgxntool-test repository, not from this repository. That repository contains the test infrastructure and development tools needed to validate changes to pgxntool. This repository contains only the framework files that get embedded into extension projects via git subtree.

+

If you want to contribute to pgxntool development, work from the pgxntool-test repository, not from this repository. That repository contains the test infrastructure and development tools needed to validate changes to pgxntool. This repository contains only the framework files that get embedded into extension projects via git subtree.

+
+
+

Changes are normally paired across both repos: a pgxntool change should come with a matching branch (same name, on the same account) and PR in pgxntool-test, and CI enforces this pairing. See the CI and Contributing section in pgxntool-test for the full workflow.

diff --git a/pgxntool/base.mk b/pgxntool/base.mk index fc9923e..a7ab93c 100644 --- a/pgxntool/base.mk +++ b/pgxntool/base.mk @@ -93,7 +93,9 @@ pgxntool_validate_yesno = $(strip \ TEST_BUILD_SQL_FILES = $(wildcard $(TESTDIR)/build/*.sql) TEST_BUILD_FILES = $(TEST_BUILD_SQL_FILES) ifdef PGXNTOOL_ENABLE_TEST_BUILD - PGXNTOOL_ENABLE_TEST_BUILD := $(call pgxntool_validate_yesno,$(PGXNTOOL_ENABLE_TEST_BUILD),PGXNTOOL_ENABLE_TEST_BUILD) + # override needed so command-line values (make VAR=YES) are normalized, not silently ignored. + # := needed for immediate evaluation of the function call (avoids infinite recursion with =). + override PGXNTOOL_ENABLE_TEST_BUILD := $(call pgxntool_validate_yesno,$(PGXNTOOL_ENABLE_TEST_BUILD),PGXNTOOL_ENABLE_TEST_BUILD) else # Auto-detect: enable if test/build/ directory has SQL files ifneq ($(strip $(TEST_BUILD_FILES)),) @@ -135,7 +137,9 @@ endif # Either approach would eliminate the ~10-line block repeated for each feature. TEST_INSTALL_SQL_FILES = $(wildcard $(TESTDIR)/install/*.sql) ifdef PGXNTOOL_ENABLE_TEST_INSTALL - PGXNTOOL_ENABLE_TEST_INSTALL := $(call pgxntool_validate_yesno,$(PGXNTOOL_ENABLE_TEST_INSTALL),PGXNTOOL_ENABLE_TEST_INSTALL) + # override needed so command-line values (make VAR=YES) are normalized, not silently ignored. + # := needed for immediate evaluation of the function call (avoids infinite recursion with =). + override PGXNTOOL_ENABLE_TEST_INSTALL := $(call pgxntool_validate_yesno,$(PGXNTOOL_ENABLE_TEST_INSTALL),PGXNTOOL_ENABLE_TEST_INSTALL) else # Auto-detect: enable if test/install/ directory has SQL files ifneq ($(strip $(TEST_INSTALL_SQL_FILES)),) @@ -292,9 +296,14 @@ endif # make results: runs `make test` and copies all result files to expected. # DO NOT RUN THIS UNLESS YOU'RE CERTAIN ALL YOUR TESTS ARE PASSING! +# +# Dependency chain (verify-results: test) guarantees test completes before verify-results +# checks regression.diffs, even under make -j. Listing both as independent prerequisites +# of results would allow them to run concurrently, letting verify-results see stale state. .PHONY: results ifeq ($(PGXNTOOL_ENABLE_VERIFY_RESULTS),yes) -results: verify-results test +verify-results: test +results: verify-results else results: test endif diff --git a/pgxntool/lib.sh b/pgxntool/lib.sh index ea38665..3cbeb0e 100644 --- a/pgxntool/lib.sh +++ b/pgxntool/lib.sh @@ -60,12 +60,21 @@ array_not_empty() { # Debug function # Usage: debug LEVEL "message" # Outputs message to stderr if DEBUG >= LEVEL -# Debug levels use multiples of 10 (10, 20, 30, 40, etc.) to allow for easy expansion +# +# LEVEL encodes how noisy/esoteric a message is -- roughly, how far you'd crank +# DEBUG before you'd actually want to see it. Higher = noisier, more rarely +# useful. This is signal-to-noise, NOT code nesting depth: a top-level line can +# warrant a high level if it's esoteric, and loop-body detail is usually high +# precisely because it's noisy. +# +# The tiers below are anchors, not strict multiples -- pick any value in range +# to fine-tune between existing calls without renumbering: # - 10: Critical errors, important warnings # - 20: Warnings, significant state changes # - 30: General debugging, function entry/exit, array operations # - 40: Verbose details, loop iterations -# - 50+: Maximum verbosity +# - 50+: Maximum verbosity (per-iteration innards) +# # Enable with: DEBUG=30 scriptname.sh debug() { local level=$1 @@ -76,3 +85,26 @@ debug() { echo "DEBUG[$level]: $message" >&2 fi } + +# Remove pgxntool's own dev-only directories from a consuming project. +# +# `git subtree` copies the ENTIRE pgxntool tree into the consumer, including +# dev-only dirs like .github/ (pgxntool's CI) and .claude/. Those are +# export-ignored from `make dist` and don't belong in a project that merely +# embeds pgxntool. (GitHub only runs workflows at the repo root, so a consumer's +# pgxntool/.github never executes anyway β€” but it's still clutter.) git subtree +# doesn't honor export-ignore, so we prune them here after a sync. +# +# Must be run from the project root (the dir containing pgxntool/). Safe to call +# repeatedly; a no-op once the dirs are gone. +prune_pgxntool_dev_dirs() { + local d + for d in .github .claude; do + [ -e "pgxntool/$d" ] || continue + echo " pgxntool/$d: pruning (pgxntool dev-only, not for embedding projects)" + # Stage the removal if tracked; rm -rf guarantees it's gone even if not. + # || : keeps this best-effort under `set -e` (rm -rf is the real cleanup). + git rm -rq --ignore-unmatch "pgxntool/$d" >/dev/null 2>&1 || : + rm -rf "pgxntool/$d" + done +} diff --git a/pgxntool/pgtle.sh b/pgxntool/pgtle.sh index 3b948e8..124ca84 100755 --- a/pgxntool/pgtle.sh +++ b/pgxntool/pgtle.sh @@ -156,7 +156,7 @@ MODULE_PATHNAME="" VERSION_FILES=() UPGRADE_FILES=() -debug 1 "Global arrays initialized: VERSION_FILES=${#VERSION_FILES[@]}, UPGRADE_FILES=${#UPGRADE_FILES[@]}" +debug 30 "Global arrays initialized: VERSION_FILES=${#VERSION_FILES[@]}, UPGRADE_FILES=${#UPGRADE_FILES[@]}" PGTLE_VERSION="" # Empty = generate all GET_DIR_VERSION="" # For --get-dir option @@ -463,7 +463,15 @@ parse_control_file() { # Order matters: stripping quotes before removing comments leaves a rogue # trailing quote for values like 'version' # note. See issue #25. value="${value%%#*}" # Remove trailing comments - value="${value%% }" # Trim trailing whitespace + # Trim all trailing whitespace (spaces and tabs). The prior %% pattern with + # a literal space only removed one character; multiple spaces or a tab before + # the comment (e.g., 'value' # note or 'value'$'\t'# note) would leave + # stray whitespace that breaks the quote-strip below. + if [[ "$value" =~ ^(.*[^[:space:]])[[:space:]]*$ ]]; then + value="${BASH_REMATCH[1]}" + else + value="" + fi # Strip quotes if present (both single and double) value="${value#\'}" @@ -513,21 +521,21 @@ parse_control_file() { discover_sql_files() { echo "Discovering SQL files for extension: $EXTENSION" >&2 - debug 2 "discover_sql_files: Starting discovery for extension: $EXTENSION" + debug 30 "discover_sql_files: Starting discovery for extension: $EXTENSION" # Ensure default_version file exists and has content if base file exists # This handles the case where make all hasn't generated it yet, or it exists but is empty local default_version_file="sql/${EXTENSION}--${DEFAULT_VERSION}.sql" local base_file="sql/${EXTENSION}.sql" if [ -f "$base_file" ] && ([ ! -f "$default_version_file" ] || [ ! -s "$default_version_file" ]); then - debug 3 "discover_sql_files: Creating default_version file from base file" + debug 40 "discover_sql_files: Creating default_version file from base file" cp "$base_file" "$default_version_file" fi # Find versioned files: sql/{ext}--{version}.sql # Use find to get proper null-delimited output, then filter out upgrade scripts VERSION_FILES=() # Reset array - debug 3 "discover_sql_files: Reset VERSION_FILES array" + debug 40 "discover_sql_files: Reset VERSION_FILES array" while IFS= read -r -d '' file; do local basename=$(basename "$file" .sql) local dash_count=$(echo "$basename" | grep -o -- "--" | wc -l | tr -d '[:space:]') @@ -545,7 +553,7 @@ discover_sql_files() { # Find upgrade scripts: sql/{ext}--{ver1}--{ver2}.sql # These have TWO occurrences of "--" in the filename UPGRADE_FILES=() # Reset array - debug 3 "discover_sql_files: Reset UPGRADE_FILES array" + debug 40 "discover_sql_files: Reset UPGRADE_FILES array" while IFS= read -r -d '' file; do # Empty upgrade files are allowed (no-op upgrades) local basename=$(basename "$file" .sql) @@ -566,15 +574,15 @@ discover_sql_files() { echo " - $f" >&2 done - debug 3 "discover_sql_files: Checking UPGRADE_FILES array, count=${#UPGRADE_FILES[@]}" + debug 40 "discover_sql_files: Checking UPGRADE_FILES array, count=${#UPGRADE_FILES[@]}" if array_not_empty "${#UPGRADE_FILES[@]}"; then echo " Found ${#UPGRADE_FILES[@]} upgrade script(s):" >&2 - debug 2 "discover_sql_files: Iterating over ${#UPGRADE_FILES[@]} upgrade files" + debug 30 "discover_sql_files: Iterating over ${#UPGRADE_FILES[@]} upgrade files" for f in "${UPGRADE_FILES[@]}"; do echo " - $f" >&2 done else - debug 2 "discover_sql_files: No upgrade files found" + debug 30 "discover_sql_files: No upgrade files found" fi } @@ -752,7 +760,7 @@ generate_install_update_path() { generate_pgtle_sql() { local pgtle_version="$1" - debug 2 "generate_pgtle_sql: Starting for version $pgtle_version, extension $EXTENSION" + debug 30 "generate_pgtle_sql: Starting for version $pgtle_version, extension $EXTENSION" # Get capability using function (compatible with bash < 4.0) local capability=$(get_pgtle_capability "$pgtle_version") @@ -761,9 +769,9 @@ generate_pgtle_sql() { # Ensure arrays are initialized (defensive programming) # Arrays should already be initialized at top level, but ensure they exist - debug 3 "generate_pgtle_sql: Checking array initialization" - debug 2 "generate_pgtle_sql: VERSION_FILES is ${VERSION_FILES+set}, count=${#VERSION_FILES[@]}" - debug 2 "generate_pgtle_sql: UPGRADE_FILES is ${UPGRADE_FILES+set}, count=${#UPGRADE_FILES[@]}" + debug 40 "generate_pgtle_sql: Checking array initialization" + debug 30 "generate_pgtle_sql: VERSION_FILES is ${VERSION_FILES+set}, count=${#VERSION_FILES[@]}" + debug 30 "generate_pgtle_sql: UPGRADE_FILES is ${UPGRADE_FILES+set}, count=${#UPGRADE_FILES[@]}" if [ -z "${VERSION_FILES+set}" ]; then echo "WARNING: VERSION_FILES not set, initializing" >&2 @@ -822,17 +830,17 @@ EOF # Install all upgrade paths local upgrade_count=${#UPGRADE_FILES[@]} - debug 3 "generate_pgtle_sql: upgrade_count=$upgrade_count" + debug 40 "generate_pgtle_sql: upgrade_count=$upgrade_count" if [ "$upgrade_count" -gt 0 ]; then - debug 2 "generate_pgtle_sql: Processing $upgrade_count upgrade path(s)" + debug 30 "generate_pgtle_sql: Processing $upgrade_count upgrade path(s)" local i=0 while [ "$i" -lt "$upgrade_count" ]; do - debug 4 "generate_pgtle_sql: Processing upgrade file $i: ${UPGRADE_FILES[$i]}" + debug 50 "generate_pgtle_sql: Processing upgrade file $i: ${UPGRADE_FILES[$i]}" generate_install_update_path "${UPGRADE_FILES[$i]}" i=$((i + 1)) done else - debug 2 "generate_pgtle_sql: No upgrade paths to process" + debug 30 "generate_pgtle_sql: No upgrade paths to process" fi cat </dev/null 2>&1 || die 1 "Not in a git repository. Run from your project root." # The old commit is the pgxntool subtree HEAD before the pull; update-setup-files.sh # needs it as the merge base for files that were copied out of pgxntool. diff --git a/pgxntool/setup.sh b/pgxntool/setup.sh index 0a67f0e..54b3633 100755 --- a/pgxntool/setup.sh +++ b/pgxntool/setup.sh @@ -7,7 +7,9 @@ trap 'echo "Error on line ${LINENO}"' ERR PGXNTOOL_DIR="$(dirname "${BASH_SOURCE[0]}")" source "$PGXNTOOL_DIR/lib.sh" -[ -d .git ] || git init +# Use rev-parse, not [ -d .git ]: in a worktree .git is a file, not a directory, +# so the old check would wrongly re-run `git init` inside a valid working tree. +git rev-parse --git-dir >/dev/null 2>&1 || git init if ! git diff --cached --exit-code; then echo "Git repository is not clean; please commit and try again." >&2 diff --git a/pgxntool/update-setup-files.sh b/pgxntool/update-setup-files.sh index 94ae75d..f814605 100755 --- a/pgxntool/update-setup-files.sh +++ b/pgxntool/update-setup-files.sh @@ -146,9 +146,10 @@ process_symlink() { old_commit=$1 -# Verify we're in a git repo with pgxntool subtree +# Verify we're in a git repo with pgxntool subtree. +# Use rev-parse, not [ -d .git ]: in a worktree .git is a file, not a directory. [[ -d "pgxntool" ]] || die 1 "pgxntool directory not found. Run from project root." -[[ -d ".git" ]] || die 1 "Not in a git repository." +git rev-parse --git-dir >/dev/null 2>&1 || die 1 "Not in a git repository." # Verify the old commit is valid if ! git cat-file -e "${old_commit}^{commit}" 2>/dev/null; then @@ -172,5 +173,9 @@ for entry in "${SETUP_SYMLINKS[@]}"; do process_symlink "$dest" "$target" done +# Prune pgxntool's own dev-only dirs (.github/, .claude/) that the subtree pull +# re-introduces but that don't belong in a consuming project (see lib.sh). +prune_pgxntool_dev_dirs + echo echo "Done. Review changes with 'git diff' and commit when ready." diff --git a/sql/cat_tools--0.2.2--0.3.0.sql.in b/sql/cat_tools--0.2.2--0.3.0.sql.in new file mode 100644 index 0000000..60445bc --- /dev/null +++ b/sql/cat_tools--0.2.2--0.3.0.sql.in @@ -0,0 +1,869 @@ +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 in 0.2.2: +-- 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 are part of the 0.2.1β†’0.2.2 upgrade. + +-- 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..d26da8f --- /dev/null +++ b/sql/cat_tools--0.3.0.sql.in @@ -0,0 +1,1981 @@ +@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 bf119eb..d26da8f 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; @@ -67,7 +65,7 @@ CREATE OR REPLACE VIEW _cat_tools.pg_class_v AS LEFT JOIN pg_namespace n ON( n.oid = c.relnamespace ) ; $fmt$ - , __cat_tools.omit_column('pg_catalog.pg_class') + , __cat_tools.omit_column('pg_catalog.pg_class', array['oid', 'relhasoids', 'relhaspkey']) )); REVOKE ALL ON _cat_tools.pg_class_v FROM public; @@ -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 input_arg_types; + 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' @@ -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,64 +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@ @@ -766,7 +1164,7 @@ $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']) + , __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; @@ -854,8 +1252,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 +1626,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..203caa0 --- /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.2β†’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..581c4ea --- /dev/null +++ b/test/install/load.sql @@ -0,0 +1,148 @@ +/* + * Single, committed-once installer for the test suite. + * + * pgxntool's test/install feature runs this file COMMITTED, in its own + * pg_regress session, BEFORE the main pgTAP suite (via a generated schedule). + * Because the state it creates is committed, it persists into every test in + * the suite. pgTAP wraps each test/sql/ file in a transaction that is rolled + * back, so the objects installed here are the ONLY committed copy: tests read + * them (create temp tables, SET ROLE, etc., all rolled back) but never modify + * or drop them. This runs ONCE instead of per-test, which is a real time saver + * for extensions with many dependencies or large install scripts. + * + * This is the ONE place the suite's dependencies are installed: + * - the cat_tools extension (fresh or upgrade, see below) + * - the test roles and their grants + * deps.sql (run per-test) no longer installs anything; it only sets the psql + * variables the suite references. + * + * Two 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; TEST_LOAD_SOURCE=upgrade selects upgrade): + * - fresh (default): plain CREATE EXTENSION cat_tools (current version). + * - upgrade: CREATE EXTENSION at the oldest cleanly-installable version and + * ALTER EXTENSION UPDATE to the current version. + * + * Why the upgrade must be committed here (not done per-test in deps.sql): 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 -- which mirrors a real + * production upgrade (ALTER EXTENSION UPDATE commits, then later transactions + * use the new values). Reusing the SAME expected output in both modes asserts + * that an upgraded database behaves identically to a fresh install; any diff + * in upgrade mode is a genuine fresh-vs-upgrade divergence. + * + * 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 a backward-compat floor for the WIDEST update path + * we can exercise -- not a claim that we only care about 0.2.2. The update + * always targets the CURRENT version (default_version), never a hardcoded + * one. + * - PG12 is the PostgreSQL floor: ALTER TYPE ... ADD VALUE cannot run inside + * a transaction block (or an extension update script) at all before PG12. + * CI restricts upgrade mode accordingly. + */ +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.drop_role() text argument). + */ +\i test/roles.sql + +/* + * Mode selection. The Makefile always exports cat_tools.test_load_mode via + * PGOPTIONS (fresh or upgrade). 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 fresh and running the wrong suite. The DO block then rejects + * any value other than fresh/upgrade with a clear message. + */ +SELECT current_setting('cat_tools.test_load_mode') AS cat_tools_test_load_mode +\gset + +DO $$ +BEGIN + IF current_setting('cat_tools.test_load_mode') NOT IN ('fresh', 'upgrade') THEN + RAISE EXCEPTION + 'cat_tools.test_load_mode must be ''fresh'' or ''upgrade'', got ''%''' + , current_setting('cat_tools.test_load_mode') + ; + END IF; +END +$$; + +/* + * Drop-first: a re-run on an existing cluster must install the newest build, + * never reuse stale objects left by a previous run. 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 upgrade 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 now mixed-case test roles. + * cat_tools__usage is not a test role (the extension script creates it) but is + * routed through the same helper so an upgrade-mode re-run starts clean. + */ +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'); + +SELECT :'cat_tools_test_load_mode' = 'upgrade' AS cat_tools_upgrade_mode +\gset + +\if :cat_tools_upgrade_mode +CREATE EXTENSION cat_tools VERSION '0.2.2'; +/* + * Suppress the deprecation NOTICEs the update scripts emit, matching the + * approach used by test/build/upgrade.sql. ALTER EXTENSION UPDATE with no + * target version updates to the current default_version. + */ +SET client_min_messages = ERROR; +ALTER EXTENSION cat_tools UPDATE; +SET client_min_messages = WARNING; +\else +CREATE EXTENSION cat_tools; +\endif + +/* + * Test roles and grants the suite depends on. Formerly created per-test in + * deps.sql; now committed here once. Names come from test/roles.sql; the + * mixed-case identifiers must be double-quoted via :"var". + */ +CREATE ROLE :"no_use_role"; +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..f65ee9e --- /dev/null +++ b/test/roles.sql @@ -0,0 +1,20 @@ +/* + * 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 are deliberately mixed-case so they REQUIRE double-quoting: any site + * that interpolates the bare identifier (:use_role) folds it to lower case and + * fails 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