Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
include pgxntool/base.mk

PGXNTOOL_ENABLE_TEST_INSTALL = yes

# ------------------------------------------------------------------------------
# TEST_LOAD_SOURCE: how test/install/load.sql gets the extension to its
# target state (fresh/update/existing). See test/install/load.sql for what
# each mode actually does.
# ------------------------------------------------------------------------------
TEST_LOAD_SOURCE ?= fresh
ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update existing),)
$(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)')
Comment on lines +11 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject wildcard and multi-word load modes.

Line 11 reverses filter’s pattern/text arguments, so values such as % or fresh update pass validation and are exported through PGOPTIONS. Validate an exact, single allowed token instead.

Proposed fix
-ifneq ($(filter $(TEST_LOAD_SOURCE),fresh update existing),)
+ifneq ($(words $(TEST_LOAD_SOURCE)),1)
+$(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)')
+endif
+ifeq ($(filter fresh update existing,$(TEST_LOAD_SOURCE)),)
 $(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)')
 endif
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 11 - 12, Correct the TEST_LOAD_SOURCE validation in
the Makefile so it checks the variable value against the exact allowed tokens
fresh, update, and existing, rather than treating it as a filter pattern. Ensure
wildcard values and multi-word values such as “fresh update” fail validation
before being exported through PGOPTIONS.

endif

# Only meaningful in 'update' mode; empty TO means "update to current".
TEST_UPDATE_FROM ?= 0.5.0
TEST_UPDATE_TO ?=

# Export unconditionally -- load.sql must never treat "absent" as "fresh".
export PGOPTIONS := $(PGOPTIONS) -c test_factory.test_load_mode=$(TEST_LOAD_SOURCE) -c test_factory.test_update_from=$(TEST_UPDATE_FROM) -c test_factory.test_update_to=$(TEST_UPDATE_TO)

# make test-update: convenience wrapper. Must re-invoke $(MAKE) (not just
# depend on test) so the parse-time TEST_LOAD_SOURCE validation above
# re-evaluates for the child invocation.
.PHONY: test-update
test-update:
$(MAKE) test TEST_LOAD_SOURCE=update

# Hook for test to ensure dependencies in control file are set correctly
testdeps: check_control

Expand Down
55 changes: 53 additions & 2 deletions test/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,61 @@ The test_factory extension uses **pgTAP** (PostgreSQL's unit testing framework)
### Test Helpers
- `test/helpers/setup.sql` - Test environment initialization and pgTAP setup
- `test/helpers/create.sql` - Test data registration and security validation
- `test/helpers/create_extension.sql` - Extension creation wrapper
- `test/helpers/deps.sql` - Test dependency management
- `test/helpers/create_extension.sql` - Extension creation wrapper; skips
`CREATE EXTENSION` when `test/install/load.sql` already installed it
(`test_load_mode` is not `fresh`)
- `test/helpers/deps.sql` - Test dependency management (`\i`'s `test/roles.sql`)
- `test/roles.sql` - Single source of truth for test-only role names
- Other helper files for role management and pgTAP integration

## Load Modes (`TEST_LOAD_SOURCE`)

`test/install/load.sql` runs once, committed, before the regular test files
(pgxntool's `PGXNTOOL_ENABLE_TEST_INSTALL` feature), so its state survives
into every test file. `TEST_LOAD_SOURCE` (default `fresh`) picks how the
extension gets to its target state:

- **fresh** (default) - `load.sql` does nothing extra; `test/sql/*.sql`
install the extension themselves, exactly as before this feature existed.
- **update** - `load.sql` does `CREATE EXTENSION test_factory VERSION
:from` then `ALTER EXTENSION UPDATE` (`TEST_UPDATE_FROM`/`TEST_UPDATE_TO`
make vars). test_factory has only ever shipped one version (0.5.0), so
this is a no-op today -- the mechanism exists for when a second version
ships, but no CI job drives it yet. `make test-update` is a shorthand for
`make test TEST_LOAD_SOURCE=update`.
- **existing** - the extension is already installed (a real `pg_upgrade`
target, or an out-of-band update) -- `load.sql` only asserts it's present
at the current version, plants a dependency guard (see below), and
proves it. `test/sql/install.sql` and the install/dependency-order part of
`test/sql/pgtap.sql` are skipped in this mode (see the `\if :is_existing`
branches in those files) since they'd otherwise defeat the guard by doing
their own from-scratch drop/recreate.

Run against a real pre-existing install with:
```
make test TEST_LOAD_SOURCE=existing CONTRIB_TESTDB=<db> \
EXTRA_REGRESS_OPTS=--use-existing PGXNTOOL_ENABLE_TEST_BUILD=no
```
Comment on lines +53 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the command example lint-clean.

Add blank lines around the fence and declare the shell language.

Proposed fix
   Run against a real pre-existing install with:
-  ```
+
+  ```bash
   make test TEST_LOAD_SOURCE=existing CONTRIB_TESTDB=<db> \
     EXTRA_REGRESS_OPTS=--use-existing PGXNTOOL_ENABLE_TEST_BUILD=no
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
Run against a real pre-existing install with:
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 54-54: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 54-54: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/CLAUDE.md` around lines 53 - 57, Update the command example near the
real pre-existing install instructions by declaring its fenced block as bash and
adding blank lines immediately before and after the fence, preserving the
command unchanged.

Source: Linters/SAST tools


`existing` mode legitimately produces different (but equally valid)
output than `fresh`/`update` for `base`/`install`/`pgtap` (skipped
sections, a skipped role-restore check), so it has alternate expected
files: `test/expected/{base,install,pgtap}_1.out` (pg_regress's numbered
alternate-expected-file convention).

### Dependency Guard

Planted only in `existing` mode (see `load.sql`): a view in schema
`test_factory_drop_guard` depending on `tf.tap(text,text)` blocks a
non-CASCADE `DROP EXTENSION test_factory_pgtap`. `test_factory` itself
doesn't need an artificial guard -- `test_factory_pgtap`'s own control file
(`requires = 'pgtap, test_factory'`) already blocks a non-CASCADE
`DROP EXTENSION test_factory` as long as `test_factory_pgtap` is installed;
`load.sql` proves that natural protection too. The point of the guard: in
`existing` mode, nothing else stops a stray drop (or a logic bug that falls
through to the fresh/update branch) from silently destroying the real
upgraded/updated objects this mode exists to test.

## Test Coverage Analysis

### Core Functionality Tests (`base.sql`)
Expand Down
25 changes: 25 additions & 0 deletions test/expected/base_1.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
\set ECHO none
Creating extension test_factory
test_factory already installed -- skipping CREATE EXTENSION (test_load_mode is not fresh)
ok 1 - Register test customers
ok 2 - Create function customer__add
ok 3 - Register test invoices
ok 4 - Ensure original_role temp table was dropped
ok 5 # SKIP role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh)
ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog
ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog
ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog
ok 9 - Security definer function _tf.table_create has search_path=pg_catalog
ok 10 - Security definer function _tf.get has search_path=pg_catalog
ok 11 - customer table is empty
ok 12 - invoice table is empty
ok 13 - invoice factory output
ok 14 - invoice table content
ok 15 - customer table content
ok 16 - invoice factory second call
ok 17 - invoice table content stayed constant
ok 18 - customer table content stayed constant
ok 19 - Test function factory
ok 20 - customer table has new row
ok 21 - truncate invoice
ok 22 - invoice factory get remains the same after truncate
2 changes: 2 additions & 0 deletions test/expected/install_1.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
\set ECHO none
ok 1 - install.sql skipped under test_load_mode=existing -- see comment in test/sql/install.sql
14 changes: 14 additions & 0 deletions test/expected/pgtap_1.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
\set ECHO none
ok 1 - Register test customers
ok 2 - Create function customer__add
ok 3 - Register test invoices
ok 4 - Ensure original_role temp table was dropped
ok 5 # SKIP role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh)
ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog
ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog
ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog
ok 9 - Security definer function _tf.table_create has search_path=pg_catalog
ok 10 - Security definer function _tf.get has search_path=pg_catalog
ok 11 - Get test data set "base" for table invoice
ok 12 - Get test data set "base" for table invoice
ok 13 - Ensure we get sane error for a non-existent table
22 changes: 18 additions & 4 deletions test/helpers/create.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
SET ROLE = DEFAULT;
CREATE ROLE test_role;
GRANT USAGE ON SCHEMA tap TO test_role;
/*
* test_role itself is created once, idempotently, by test/install/load.sql
* (test/roles.sql is the single source of truth for the name; \i'd via
* test/helpers/deps.sql).
*/
GRANT USAGE ON SCHEMA tap TO :test_role;
/*
* DO NOT GRANT test_role TO test_factory__owner; the whole point test_role is
* to check for security problems.
*/

CREATE SCHEMA test AUTHORIZATION test_role;
SET ROLE = test_role;
CREATE SCHEMA test AUTHORIZATION :test_role;
SET ROLE = :test_role;
SET search_path = test, tap;

CREATE TABLE customer(
Expand Down Expand Up @@ -79,11 +83,21 @@ SELECT hasnt_table(
, 'Ensure original_role temp table was dropped'
);

/*
* Only meaningful when this session actually ran CREATE EXTENSION itself
* (test_load_mode=fresh; the tables don't exist under update/existing,
* where test/install/load.sql installed/updated the extension earlier).
*/
SELECT to_regclass('pg_temp.pre_install_role') IS NOT NULL AS has_role_capture \gset
\if :has_role_capture
SELECT is(
(SELECT * FROM post_install_role)
, (SELECT * FROM pre_install_role)
, 'Ensure role is put back after install'
);
\else
SELECT skip('role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh)', 1);
\endif

SELECT cmp_ok(
proconfig
Expand Down
19 changes: 18 additions & 1 deletion test/helpers/create_extension.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
\echo Creating extension :extension_name
-- No IF NOT EXISTS because we'll be confused if we're not loading the new stuff
/*
* In 'update'/'existing' mode, test/install/load.sql already installed (and,
* for 'update', updated) the extensions in its own earlier committed
* session -- skip re-creating here instead of erroring or, worse, silently
* replacing the state those modes exist to test. In 'fresh' mode (the only
* mode test/install/load.sql leaves untouched), keep the original
* behavior: no IF NOT EXISTS, so we're confused loudly if something's
* already there instead of silently testing stale state.
*/
SELECT
current_setting('test_factory.test_load_mode') <> 'fresh'
AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = :'extension_name')
AS already_installed
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the SQLFluff layout violation.

Line 11 places WHERE inline inside the subquery, which violates enforced LT14.

Proposed fix
-  AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = :'extension_name')
+  AND EXISTS (
+    SELECT 1
+    FROM pg_extension
+    WHERE extname = :'extension_name'
+  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SELECT
current_setting('test_factory.test_load_mode') <> 'fresh'
AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = :'extension_name')
AS already_installed
SELECT
current_setting('test_factory.test_load_mode') <> 'fresh'
AND EXISTS (
SELECT 1
FROM pg_extension
WHERE extname = :'extension_name'
)
AS already_installed
🧰 Tools
🪛 SQLFluff (4.2.2)

[error] 11-11: The 'WHERE' keyword should always start a new line.

(LT14)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/helpers/create_extension.sql` around lines 9 - 12, Adjust the subquery
formatting in the already_installed query so the WHERE clause begins on its own
line, satisfying SQLFluff LT14 while preserving the existing existence check and
result.

Source: Linters/SAST tools

\gset
\if :already_installed
\echo :extension_name already installed -- skipping CREATE EXTENSION (test_load_mode is not fresh)
\else
CREATE TEMP TABLE pre_install_role AS SELECT current_user;
GRANT SELECT ON pre_install_role TO public; -- In case role is different
CREATE EXTENSION :extension_name;
CREATE TEMP TABLE post_install_role AS SELECT current_user;
GRANT SELECT ON post_install_role TO public; -- In case role is different
\endif
1 change: 1 addition & 0 deletions test/helpers/deps.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
\i test/roles.sql
1 change: 1 addition & 0 deletions test/install/load.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
\set ECHO none
Loading
Loading