diff --git a/META.in.json b/META.in.json index 6be26e2..461556b 100644 --- a/META.in.json +++ b/META.in.json @@ -2,7 +2,7 @@ "name": "test_factory", "abstract": "Framework for managing test data", "description": "Provides functions for registering commands to create test data and obtaining data during testing", - "version": "0.5.0", + "version": "1.0.0", "maintainer": "Jim Nasby ", "license": {"BSD 2 Clause": "http://opensource.org/licenses/bsd-license.php"}, @@ -12,31 +12,31 @@ "abstract": "Framework for managing test data", "file": "sql/test_factory.sql", "docfile": "doc/test_factory.asc", - "version": "0.5.0" + "version": "1.0.0" }, "test_factory_pgtap": { "abstract": "pgTap extension for test_factory", "file": "sql/test_factory_pgtap.sql", "docfile": "doc/test_factory.asc", - "version": "0.1.0" + "version": "1.0.0" } }, "release_status": "stable", - "generated_by": "Jim Nasby ", + "generated_by": "Jim Nasby ", "tags": [ "testing", "unit test", "tests", "test data", "test factory" ], "resources": { - "homepage": "http://github.com/BlueTreble/test_factory/", + "homepage": "http://github.com/Postgres-Extensions/test_factory/", "bugtracker": { - "web": "http://github.com/BlueTreble/test_factory/issues" + "web": "http://github.com/Postgres-Extensions/test_factory/issues" }, "repository": { - "url": "git://github.com/BlueTreble/test_factory.git", - "web": "http://github.com/BlueTreble/test_factory/", + "url": "git://github.com/Postgres-Extensions/test_factory.git", + "web": "http://github.com/Postgres-Extensions/test_factory/", "type": "git" } }, diff --git a/META.json b/META.json index c8db504..3378791 100644 --- a/META.json +++ b/META.json @@ -5,7 +5,7 @@ "name": "test_factory", "abstract": "Framework for managing test data", "description": "Provides functions for registering commands to create test data and obtaining data during testing", - "version": "0.5.0", + "version": "1.0.0", "maintainer": "Jim Nasby ", "license": {"BSD 2 Clause": "http://opensource.org/licenses/bsd-license.php"}, @@ -15,31 +15,31 @@ "abstract": "Framework for managing test data", "file": "sql/test_factory.sql", "docfile": "doc/test_factory.asc", - "version": "0.5.0" + "version": "1.0.0" }, "test_factory_pgtap": { "abstract": "pgTap extension for test_factory", "file": "sql/test_factory_pgtap.sql", "docfile": "doc/test_factory.asc", - "version": "0.1.0" + "version": "1.0.0" } }, "release_status": "stable", - "generated_by": "Jim Nasby ", + "generated_by": "Jim Nasby ", "tags": [ "testing", "unit test", "tests", "test data", "test factory" ], "resources": { - "homepage": "http://github.com/BlueTreble/test_factory/", + "homepage": "http://github.com/Postgres-Extensions/test_factory/", "bugtracker": { - "web": "http://github.com/BlueTreble/test_factory/issues" + "web": "http://github.com/Postgres-Extensions/test_factory/issues" }, "repository": { - "url": "git://github.com/BlueTreble/test_factory.git", - "web": "http://github.com/BlueTreble/test_factory/", + "url": "git://github.com/Postgres-Extensions/test_factory.git", + "web": "http://github.com/Postgres-Extensions/test_factory/", "type": "git" } }, diff --git a/pgxntool/HISTORY.asc b/pgxntool/HISTORY.asc index bedc0b5..4b9a085 100644 --- a/pgxntool/HISTORY.asc +++ b/pgxntool/HISTORY.asc @@ -1,10 +1,37 @@ +1.1.1 +----- +== Fix pg_tle exception handler and empty upgrade files +The exception handler for `uninstall_extension()` now correctly catches +`no_data_found` (P0002) instead of `undefined_object` (42704). Empty upgrade +files are now treated as valid no-op upgrades for version bumps. Added +`ON_ERROR_STOP=1` to `run_pgtle_sql()` so psql errors propagate correctly. + +1.1.0 +----- +== Use unique database names for tests +Tests now use a unique database name based on the project name and a hash of the +current directory. This prevents test conflicts when running tests for multiple +projects in parallel. + +== Add 3-way merge support for setup files after pgxntool-sync +New `update-setup-files.sh` script handles merging changes to files initially +copied by `setup.sh` (`.gitignore`, `test/deps.sql`). After running `make +pgxntool-sync`, the script performs a 3-way merge if both you and pgxntool have +modified the same file, using git's native conflict markers for resolution. + + 1.0.0 ----- == Fix broken multi-extension support -Prior to this fix, distributions with multiple extensions or extensions with versions different from the PGXN distribution version were completely broken. Extension versions are now correctly read from each `.control` file's `default_version` instead of using META.json's distribution version. +Prior to this fix, distributions with multiple extensions or extensions with +versions different from the PGXN distribution version were completely broken. +Extension versions are now correctly read from each `.control` file's +`default_version` instead of using META.json's distribution version. == Add pg_tle support -New `make pgtle` target generates pg_tle registration SQL for extensions. Supports pg_tle version ranges (1.0.0-1.4.0, 1.4.0-1.5.0, 1.5.0+) with appropriate API calls for each range. See README for usage. +New `make pgtle` target generates pg_tle registration SQL for extensions. +Supports pg_tle version ranges (1.0.0-1.4.0, 1.4.0-1.5.0, 1.5.0+) with +appropriate API calls for each range. See README for usage. == Use git tags for distribution versioning The `tag` and `rmtag` targets now create/delete git tags instead of branches. @@ -16,7 +43,9 @@ The `--load-language` option was removed from `pg_regress` in 13. As part of this change, you will want to review the changes to test/deps.sql. === Support asciidoc documentation targets -By default, if asciidoctor or asciidoc exists on the system, any files in doc/ that end in .adoc or .asciidoc will be processed to html. +By default, if asciidoctor or asciidoc exists on the system, any files in doc/ +that end in .adoc or .asciidoc will be processed to html. + See the README for full details. === Support 9.2 diff --git a/pgxntool/base.mk b/pgxntool/base.mk index b03a5cc..9b621df 100644 --- a/pgxntool/base.mk +++ b/pgxntool/base.mk @@ -64,6 +64,13 @@ TEST__SOURCE__SQL_FILES = $(patsubst $(TESTDIR)/input/%.source,$(TESTDIR)/sql/% TEST__SOURCE__EXPECTED_FILES = $(patsubst $(TESTDIR)/output/%.source,$(TESTDIR)/expected/%.out,$(TEST__SOURCE__OUTPUT_FILES)) REGRESS = $(sort $(notdir $(subst .source,,$(TEST_FILES:.sql=)))) # Sort is to get unique list REGRESS_OPTS = --inputdir=$(TESTDIR) --outputdir=$(TESTOUT) # See additional setup below + +# Generate unique database name for tests to prevent conflicts across projects +# Uses project name + first 5 chars of md5 hash of current directory +# This prevents multiple test runs in different directories from clobbering each other +REGRESS_DBHASH := $(shell echo $(CURDIR) | (md5 2>/dev/null || md5sum) | cut -c1-5) +REGRESS_DBNAME := $(or $(PGXN),regression)_$(REGRESS_DBHASH) +REGRESS_OPTS += --dbname=$(REGRESS_DBNAME) MODULES = $(patsubst %.c,%,$(wildcard src/*.c)) ifeq ($(strip $(MODULES)),) MODULES =# Set to NUL so PGXS doesn't puke @@ -291,17 +298,25 @@ print-% : ; $(info $* is $(flavor $*) variable set to "$($*)") @true # # This is setup to allow any number of pull targets by defining special # variables. pgxntool-sync-release is an example of this. -.PHONY: pgxn-sync-% +# +# After the subtree pull, we run update-setup-files.sh to handle files that +# were initially copied by setup.sh (like .gitignore). This script does a +# 3-way merge if both you and pgxntool changed the file. +.PHONY: pgxntool-sync-% pgxntool-sync-%: - git subtree pull -P pgxntool --squash -m "Pull pgxntool from $($@)" $($@) + @old_commit=$$(git log -1 --format=%H -- pgxntool/) && \ + git subtree pull -P pgxntool --squash -m "Pull pgxntool from $($@)" $($@) && \ + pgxntool/update-setup-files.sh "$$old_commit" pgxntool-sync: pgxntool-sync-release # DANGER! Use these with caution. They may add extra crap to your history and # could make resolving merges difficult! pgxntool-sync-release := git@github.com:decibel/pgxntool.git release pgxntool-sync-stable := git@github.com:decibel/pgxntool.git stable +pgxntool-sync-master := git@github.com:decibel/pgxntool.git master pgxntool-sync-local := ../pgxntool release # Not the same as PGXNTOOL_DIR! pgxntool-sync-local-stable := ../pgxntool stable # Not the same as PGXNTOOL_DIR! +pgxntool-sync-local-master := ../pgxntool master # Not the same as PGXNTOOL_DIR! distclean: rm -f $(PGXNTOOL_distclean) diff --git a/pgxntool/lib.sh b/pgxntool/lib.sh index c3eb88e..41de512 100644 --- a/pgxntool/lib.sh +++ b/pgxntool/lib.sh @@ -4,6 +4,23 @@ # This file is meant to be sourced by other scripts, not executed directly. # Usage: source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +# ============================================================================= +# SETUP FILES CONFIGURATION +# ============================================================================= +# Files copied by setup.sh and tracked by update-setup-files.sh for sync updates. +# Format: "source_in_pgxntool:destination_in_project" +# ============================================================================= +SETUP_FILES=( + "_.gitignore:.gitignore" + "test/deps.sql:test/deps.sql" +) + +# Symlinks created by setup.sh and verified by update-setup-files.sh +# Format: "destination:target" +SETUP_SYMLINKS=( + "test/pgxntool:../pgxntool/test/pgxntool" +) + # Error function - outputs to stderr but doesn't exit # Usage: error "message" error() { diff --git a/pgxntool/pgtle.sh b/pgxntool/pgtle.sh index 8fd2d17..abbcfd4 100755 --- a/pgxntool/pgtle.sh +++ b/pgxntool/pgtle.sh @@ -330,7 +330,7 @@ run_pgtle_sql() { if [ -f "$sql_file" ]; then found=1 echo "Running $sql_file..." >&2 - psql --no-psqlrc --file="$sql_file" || exit 1 + psql --no-psqlrc -v ON_ERROR_STOP=1 --file="$sql_file" || exit 1 fi done @@ -545,10 +545,7 @@ discover_sql_files() { UPGRADE_FILES=() # Reset array debug 30 "discover_sql_files: Reset UPGRADE_FILES array" while IFS= read -r -d '' file; do - # Error on empty upgrade files - if [ ! -s "$file" ]; then - die 1 "Empty upgrade file found: $file" - fi + # Empty upgrade files are allowed (no-op upgrades) local basename=$(basename "$file" .sql) local dash_count=$(echo "$basename" | grep -o -- "--" | wc -l | tr -d '[:space:]') if [ "$dash_count" -eq 2 ]; then @@ -615,6 +612,7 @@ wrap_sql_content() { validate_delimiter "$sql_file" # Output wrapped SQL with proper indentation + # Empty files are valid (no-op upgrades) echo " ${PGTLE_DELIMITER}" cat "$sql_file" echo " ${PGTLE_DELIMITER}" @@ -661,17 +659,17 @@ generate_header() { * - Upgrade paths: ${upgrade_count} path(s) * - Default version: ${DEFAULT_VERSION} * - * Installation instructions: - * 1. Ensure pg_tle is installed: - * CREATE EXTENSION pg_tle; + * Installation: + * Recommended: make run-pgtle + * (or: pgxntool/pgtle.sh --run) * - * 2. Ensure you have pgtle_admin role: - * GRANT pgtle_admin TO your_username; + * This automatically detects your pg_tle version and runs the correct file. * - * 3. Run this file: - * psql -f $(basename "$output_file") + * Prerequisites: + * - pg_tle extension installed (CREATE EXTENSION pg_tle;) + * - pgtle_admin role (GRANT pgtle_admin TO your_username;) * - * 4. Create the extension: + * After registration, create the extension: * CREATE EXTENSION ${EXTENSION}; * * Version compatibility: @@ -799,8 +797,8 @@ DO \$\$ BEGIN PERFORM pgtle.uninstall_extension('${EXTENSION}'); EXCEPTION - WHEN undefined_object THEN - -- Extension might not exist yet + WHEN no_data_found THEN + -- Extension not registered yet (pg_tle raises P0002, not 42704) NULL; END \$\$; diff --git a/pgxntool/setup.sh b/pgxntool/setup.sh index 08751f1..0a67f0e 100755 --- a/pgxntool/setup.sh +++ b/pgxntool/setup.sh @@ -39,21 +39,44 @@ safecp () { fi } -safecp pgxntool/_.gitignore .gitignore -safecp pgxntool/META.in.json META.in.json +# ============================================================================= +# SETUP FILES +# ============================================================================= +# SETUP_FILES and SETUP_SYMLINKS are defined in lib.sh +# These are also used by update-setup-files.sh for sync updates. +# ============================================================================= + +# Copy tracked setup files (defined in lib.sh) +for entry in "${SETUP_FILES[@]}"; do + src="pgxntool/${entry%%:*}" + dest="${entry##*:}" + # Create parent directory if needed + mkdir -p "$(dirname "$dest")" + safecp "$src" "$dest" +done +# Create tracked symlinks (defined in lib.sh) +for entry in "${SETUP_SYMLINKS[@]}"; do + dest="${entry%%:*}" + target="${entry##*:}" + mkdir -p "$(dirname "$dest")" + if [ ! -e "$dest" ]; then + echo "Creating symlink $dest -> $target" + ln -s "$target" "$dest" + git add "$dest" + else + echo "$dest already exists" + fi +done + +# META.in.json and Makefile are NOT in SETUP_FILES because users heavily customize them +safecp pgxntool/META.in.json META.in.json safecreate Makefile include pgxntool/base.mk make META.json git add META.json -mkdir -p sql test src - -cd test -mkdir -p sql -safecp ../pgxntool/test/deps.sql deps.sql -[ -d pgxntool ] || ln -s ../pgxntool/test/pgxntool . -git add pgxntool +mkdir -p sql test/sql src git status echo "If you won't be creating C code then you can: diff --git a/pgxntool/update-setup-files.sh b/pgxntool/update-setup-files.sh new file mode 100755 index 0000000..94ae75d --- /dev/null +++ b/pgxntool/update-setup-files.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# +# update-setup-files.sh - Update files that were initially copied by setup.sh +# +# This script handles the 3-way merge of setup files after a pgxntool subtree +# update. It compares the old pgxntool version, new pgxntool version, and +# user's current file to determine the appropriate action: +# +# 1. If pgxntool didn't change the file: skip (nothing to do) +# 2. If user hasn't modified the file: auto-update +# 3. If both changed: 3-way merge with conflict markers +# +# Usage: update-setup-files.sh +# +# The old commit is the pgxntool subtree commit BEFORE the sync. + +set -o errexit -o errtrace -o pipefail +trap 'echo "Error on line ${LINENO}"' ERR + +PGXNTOOL_DIR="$(dirname "${BASH_SOURCE[0]}")" +source "$PGXNTOOL_DIR/lib.sh" + +# SETUP_FILES and SETUP_SYMLINKS are defined in lib.sh + +# ============================================================================= +# Functions +# ============================================================================= + +usage() { + echo "Usage: $0 " + echo + echo "Updates setup files after a pgxntool subtree sync." + echo + echo "Arguments:" + echo " old-pgxntool-commit The pgxntool commit hash BEFORE the sync" + exit 1 +} + +# Get file content from a specific commit +# Usage: get_old_content +get_old_content() { + local commit=$1 + local path=$2 + git show "${commit}:pgxntool/${path}" 2>/dev/null +} + +# Get current file content from pgxntool directory +# Usage: get_new_content +get_new_content() { + local path=$1 + cat "pgxntool/${path}" 2>/dev/null +} + +# Process a single setup file +# Usage: process_file +process_file() { + local source=$1 + local dest=$2 + local old_commit=$3 + + # Get the three versions + local old_content new_content user_content + + old_content=$(get_old_content "$old_commit" "$source") || { + debug 20 "Could not get old version of $source (new file in pgxntool?)" + old_content="" + } + + new_content=$(get_new_content "$source") || { + error "Could not read pgxntool/$source" + return 1 + } + + # Check if destination exists + if [[ ! -e "$dest" ]]; then + echo " $dest: creating (file was missing)" + cp "pgxntool/$source" "$dest" + return 0 + fi + + user_content=$(cat "$dest") + + # Step 1: Did pgxntool change this file? + if [[ "$old_content" == "$new_content" ]]; then + debug 30 "$dest: pgxntool unchanged, skipping" + return 0 + fi + + # Step 2: Did user modify their copy? + if [[ "$user_content" == "$old_content" ]]; then + echo " $dest: updated (you hadn't modified it)" + cp "pgxntool/$source" "$dest" + return 0 + fi + + # Step 3: Both changed - need 3-way merge + echo " $dest: attempting 3-way merge..." + + # Create temp files for git merge-file + local tmp_old tmp_new + tmp_old=$(mktemp) + tmp_new=$(mktemp) + trap "rm -f '$tmp_old' '$tmp_new'" RETURN + + echo "$old_content" > "$tmp_old" + echo "$new_content" > "$tmp_new" + + # git merge-file modifies the first file in place + # Returns 0 on clean merge, >0 if conflicts (but still writes result) + if git merge-file -L "yours" -L "old pgxntool" -L "new pgxntool" \ + "$dest" "$tmp_old" "$tmp_new"; then + echo " $dest: merged cleanly (please review)" + else + echo " $dest: CONFLICTS - resolve manually" + fi +} + +# Process a symlink +# Usage: process_symlink +process_symlink() { + local dest=$1 + local target=$2 + + if [[ -L "$dest" ]]; then + local current_target + current_target=$(readlink "$dest") + if [[ "$current_target" == "$target" ]]; then + debug 30 "$dest: symlink unchanged" + else + echo " $dest: symlink points to '$current_target', expected '$target'" + echo " (not auto-fixing - please check manually)" + fi + elif [[ -e "$dest" ]]; then + echo " $dest: exists but is not a symlink (expected symlink to $target)" + else + echo " $dest: creating symlink to $target" + ln -s "$target" "$dest" + fi +} + +# ============================================================================= +# Main +# ============================================================================= + +[[ $# -eq 1 ]] || usage + +old_commit=$1 + +# Verify we're in a git repo with pgxntool subtree +[[ -d "pgxntool" ]] || die 1 "pgxntool directory not found. Run from project root." +[[ -d ".git" ]] || 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 + die 1 "Invalid commit: $old_commit" +fi + +echo "Checking setup files for updates..." +echo + +# Process regular files +for entry in "${SETUP_FILES[@]}"; do + source="${entry%%:*}" + dest="${entry##*:}" + process_file "$source" "$dest" "$old_commit" +done + +# Process symlinks +for entry in "${SETUP_SYMLINKS[@]}"; do + dest="${entry%%:*}" + target="${entry##*:}" + process_symlink "$dest" "$target" +done + +echo +echo "Done. Review changes with 'git diff' and commit when ready." diff --git a/sql/test_factory--1.0.0.sql b/sql/test_factory--1.0.0.sql new file mode 100644 index 0000000..636bcb5 --- /dev/null +++ b/sql/test_factory--1.0.0.sql @@ -0,0 +1,264 @@ +/* DO NOT EDIT - AUTO-GENERATED FILE */ +CREATE TEMP TABLE original_role ON COMMIT DROP AS SELECT current_user AS original_role; + +GRANT SELECT ON pg_temp.original_role TO public; +DO $body$ +BEGIN + CREATE ROLE test_factory__owner; +EXCEPTION + WHEN duplicate_object THEN + NULL; +END +$body$; + +CREATE SCHEMA tf AUTHORIZATION test_factory__owner; +COMMENT ON SCHEMA tf IS $$Test factory. Tools for maintaining test data.$$; +GRANT USAGE ON SCHEMA tf TO public; + +CREATE SCHEMA _tf AUTHORIZATION test_factory__owner; +-- Sucks that we have to do this. Need community to separate visibility and usage. +GRANT USAGE ON SCHEMA _tf TO public; + +CREATE SCHEMA _test_factory_test_data AUTHORIZATION test_factory__owner; + +-- Need to be SU +CREATE OR REPLACE FUNCTION _tf.schema__getsert( +) RETURNS name SECURITY DEFINER SET search_path = pg_catalog LANGUAGE plpgsql AS $body$ +BEGIN + /* + IF NOT EXISTS( SELECT 1 FROM pg_namespace WHERE nspname = '_test_data' ) THEN + CREATE SCHEMA _test_data AUTHORIZATION test_factory__owner; + END IF; + */ + + RETURN '_test_factory_test_data'; +END +$body$; + +SET LOCAL ROLE test_factory__owner; + +CREATE TYPE tf.test_set AS ( + set_name text + , insert_sql text +); + +CREATE TABLE _tf._test_factory( + factory_id SERIAL NOT NULL PRIMARY KEY + , table_oid regclass NOT NULL -- Can't do a FK to a catalog + , set_name text NOT NULL + , insert_sql text NOT NULL + , UNIQUE( table_oid, set_name ) +); +SELECT pg_catalog.pg_extension_config_dump('_tf._test_factory', ''); +SELECT pg_catalog.pg_extension_config_dump('_tf._test_factory_factory_id_seq', ''); + + +CREATE OR REPLACE FUNCTION _tf.data_table_name( + table_name text -- Sanitized by tf.test_factory__get() + , set_name _tf._test_factory.set_name%TYPE +) RETURNS name LANGUAGE plpgsql AS $body$ +DECLARE + v_factory_id_text text; + v_table_name name; + + v_name name; +BEGIN + SELECT + -- Get a fixed-width representation of ID. btrim shouldn't be necessary but it is + '_' || btrim( to_char( + factory_id + -- Get a string of 0's long enough to hold a max-sized int + , repeat( '0', length( (2^31-1)::int::text ) ) + ) ) + , c.relname + INTO v_factory_id_text, v_table_name + FROM tf.test_factory__get( table_name, set_name ) f + JOIN pg_class c ON c.oid = f.table_oid + JOIN pg_namespace n ON n.oid = c.relnamespace + ; + + v_name := v_table_name || v_factory_id_text; + + -- Was the name truncated? + IF v_name <> (v_table_name || v_factory_id_text) THEN + v_name := substring( v_table_name, length(v_name) - length(v_factory_id_text ) ) + || v_factory_id_text + ; + END IF; + + RETURN v_name; +END +$body$; + + +CREATE OR REPLACE FUNCTION _tf.test_factory__get( + table_name text -- Sanitized by tf.test_factory__get() + , set_name _tf._test_factory.set_name%TYPE + , table_oid oid -- Must be passed in because of forced search_path +) RETURNS _tf._test_factory SECURITY DEFINER SET search_path = pg_catalog LANGUAGE plpgsql AS $body$ +DECLARE + v_test_factory _tf._test_factory; +BEGIN + SELECT * INTO STRICT v_test_factory + FROM _tf._test_factory tf + WHERE tf.table_oid = test_factory__get.table_oid + AND tf.set_name = test_factory__get.set_name + ; + + RETURN v_test_factory; +EXCEPTION + WHEN no_data_found THEN + RAISE 'No factory found for table "%", set name "%"', table_name, set_name; +END +$body$; +CREATE OR REPLACE FUNCTION tf.test_factory__get( + table_name text + , set_name _tf._test_factory.set_name%TYPE +) RETURNS _tf._test_factory LANGUAGE sql AS $body$ +SELECT * FROM _tf.test_factory__get(table_name, set_name, table_name::regclass) +$body$; + + +CREATE OR REPLACE FUNCTION _tf.test_factory__set( + table_oid regclass + , set_name text + , insert_sql text +) RETURNS void SECURITY DEFINER SET search_path = pg_catalog LANGUAGE plpgsql AS $body$ +BEGIN + UPDATE _tf._test_factory + SET insert_sql = test_factory__set.insert_sql + WHERE _test_factory.table_oid = test_factory__set.table_oid + AND _test_factory.set_name = test_factory__set.set_name + ; + /* + * There shouldn't be concurrency conflicts here. If there are I think it's + * better to error than UPSERT. + */ + IF NOT FOUND THEN + INSERT INTO _tf._test_factory( table_oid, set_name, insert_sql ) + VALUES( table_oid, set_name, insert_sql ) + ; + END IF; +END +$body$; + + +CREATE OR REPLACE FUNCTION tf.register( + table_name text + , test_sets tf.test_set[] +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + c_table_oid CONSTANT regclass := table_name; + v_set tf.test_set; +BEGIN + FOREACH v_set IN ARRAY test_sets LOOP + PERFORM _tf.test_factory__set( + c_table_oid + , v_set.set_name + , v_set.insert_sql + ); + END LOOP; +END +$body$; + + +CREATE OR REPLACE FUNCTION _tf.table_create( + table_name text +) RETURNS void SECURITY DEFINER SET search_path = pg_catalog LANGUAGE plpgsql AS $body$ +DECLARE + c_td_schema CONSTANT name := _tf.schema__getsert(); + sql text; +BEGIN + sql := format( + $sql$ +CREATE TABLE %I.%I AS SELECT * FROM pg_temp.%2$I; + $sql$ + , c_td_schema + , table_name + ); + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE OR REPLACE FUNCTION tf.get( + table_type anyelement + , set_name text +) RETURNS SETOF anyelement LANGUAGE plpgsql AS $body$ +DECLARE + c_table_name CONSTANT text := pg_typeof(table_type); + c_data_table_name CONSTANT name := _tf.data_table_name( c_table_name, set_name ); +BEGIN + -- SEE BELOW AS WELL + RETURN QUERY SELECT * FROM _tf.get(table_type, set_name, c_data_table_name); +EXCEPTION + WHEN undefined_table THEN + DECLARE + create_sql text; + BEGIN + -- TODO: Create temp table with caller security then create permanent table as test_factory__owner + SELECT format( + $$ +CREATE TEMP TABLE %I ON COMMIT DROP AS +WITH i AS ( + %s + ) + SELECT * + FROM i +; +GRANT SELECT ON pg_temp.%1$I TO test_factory__owner; +$$ + , c_data_table_name + , factory.insert_sql + ) + INTO create_sql + FROM tf.test_factory__get( c_table_name, set_name ) factory + ; + RAISE DEBUG 'sql = %', create_sql; + EXECUTE create_sql; + PERFORM _tf.table_create( c_data_table_name ); + + -- SEE ABOVE AS WELL + RETURN QUERY SELECT * FROM _tf.get(table_type, set_name, c_data_table_name); + + -- Can't do this in the secdef function because it doesn't own it. + EXECUTE format( 'DROP TABLE pg_temp.%I', c_data_table_name ); + END; +END +$body$; + +CREATE OR REPLACE FUNCTION _tf.get( + table_type anyelement -- Sanitized by tf.test_factory__get() + , set_name text + , data_table_name name +) RETURNS SETOF anyelement SECURITY DEFINER SET search_path = pg_catalog LANGUAGE plpgsql AS $body$ +DECLARE + c_table_name CONSTANT text := pg_typeof(table_type); + c_td_schema CONSTANT name := _tf.schema__getsert(); + + sql text; +BEGIN + sql := format( + 'SELECT * FROM %I.%I AS t' + , c_td_schema + , data_table_name + ); + RAISE DEBUG 'sql = %', sql; + + RETURN QUERY EXECUTE sql; +END +$body$; + +--select (tf.get('moo','moo')::moo).*; +DO $body$ +DECLARE + c_sql CONSTANT text := 'SET ROLE ' || (SELECT original_role FROM pg_temp.original_role); +BEGIN + --RAISE WARNING 'c_sql = %', c_sql; + EXECUTE c_sql; +END +$body$; + +DROP TABLE pg_temp.original_role; + +-- vi: expandtab ts=2 sw=2 diff --git a/sql/test_factory_pgtap--1.0.0.sql b/sql/test_factory_pgtap--1.0.0.sql new file mode 100644 index 0000000..67e2a94 --- /dev/null +++ b/sql/test_factory_pgtap--1.0.0.sql @@ -0,0 +1,41 @@ +/* DO NOT EDIT - AUTO-GENERATED FILE */ +CREATE TEMP TABLE original_role ON COMMIT DROP AS SELECT current_user AS original_role; +GRANT SELECT ON pg_temp.original_role TO public; + +SET LOCAL ROLE test_factory__owner; + +CREATE OR REPLACE FUNCTION tf.tap( + table_name text + , set_name text DEFAULT 'base' +) RETURNS SETOF text LANGUAGE plpgsql AS $body$ +DECLARE + c_table CONSTANT regclass := table_name; +BEGIN + RETURN NEXT isnt_empty( + format( + $$SELECT tf.get( NULL::%s, %L )$$ -- We assume regclass::text gives us valid output + , c_table + , set_name + ) + , format( + 'Get test data set "%s" for table %s' + , set_name + , c_table + ) + ); +END +$body$; + +-- Set role back to original value +DO $body$ +DECLARE + c_sql CONSTANT text := 'SET ROLE ' || (SELECT original_role FROM pg_temp.original_role); +BEGIN + --RAISE WARNING 'c_sql = %', c_sql; + EXECUTE c_sql; +END +$body$; + +DROP TABLE pg_temp.original_role; + +-- vi: expandtab ts=2 sw=2 diff --git a/test_factory.control b/test_factory.control index a18e438..fabea06 100644 --- a/test_factory.control +++ b/test_factory.control @@ -1,3 +1,3 @@ comment = 'A framework for managing test data' -default_version = '0.5.0' +default_version = '1.0.0' relocatable = false diff --git a/test_factory_pgtap.control b/test_factory_pgtap.control index 8e4a75f..9696b14 100644 --- a/test_factory_pgtap.control +++ b/test_factory_pgtap.control @@ -1,4 +1,4 @@ comment = 'A framework for managing test data' -default_version = '0.1.0' +default_version = '1.0.0' relocatable = false requires = 'pgtap, test_factory'