diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0348647 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: test + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: PostgreSQL ${{ matrix.pg }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + pg: [14, 15, 16, 17, 18] + env: + PG_CONFIG: /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config + # Pinned pg_tle commit (main, supports PG 12-18; no tagged release yet + # covers 18). + PG_TLE_REF: 2f4b7b34ac3e65a4c4ec358765839d5f24a910bd + PGHOST: /var/run/postgresql + PGPORT: 5499 + PGDATABASE: postgres + steps: + - uses: actions/checkout@v4 + + - name: Install PostgreSQL ${{ matrix.pg }} + run: | + sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y + # Don't let the package auto-create a "main" cluster; we make our own. + echo 'create_main_cluster = false' | sudo tee -a /etc/postgresql-common/createcluster.conf + sudo apt-get install -y --no-install-recommends \ + postgresql-${{ matrix.pg }} postgresql-server-dev-${{ matrix.pg }} \ + flex bison libkrb5-dev + + - name: Build and install pg_tle + run: | + git init -q pg_tle + git -C pg_tle fetch -q --depth 1 https://github.com/aws/pg_tle "$PG_TLE_REF" + git -C pg_tle checkout -q FETCH_HEAD + make -C pg_tle PG_CONFIG="$PG_CONFIG" + sudo make -C pg_tle install PG_CONFIG="$PG_CONFIG" + + - name: Start cluster + run: | + sudo pg_createcluster ${{ matrix.pg }} ci -p "$PGPORT" --start \ + -o shared_preload_libraries=pg_tle + # sudo resets the environment, so PGPORT has to be passed explicitly. + sudo -u postgres createuser -p "$PGPORT" -s "$USER" + + # pg_tle refuses to register an extension that also exists on the + # filesystem, so run it before `make install`. + - name: Test standalone build + run: make test-local + + - name: Test pg_tle install + run: make test-tle + + - name: Test filesystem extension + run: | + sudo make install PG_CONFIG="$PG_CONFIG" + make test-extension + + - name: Server log + if: failure() + run: sudo cat /var/log/postgresql/postgresql-${{ matrix.pg }}-ci.log diff --git a/AGENTS.md b/AGENTS.md index 4f0c760..4f86e5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ designed for LLM prompt contexts. It conforms to TOON Specification v3.3. pgtoon--0.1.sql # Canonical extension source (contains @extschema@ markers) pgtoon.control # PostgreSQL extension metadata (relocatable=false) test_pgtoon.sql # Regression suite (67 assertions) -Makefile # Build targets: tle (default), local, clean, help +Makefile # Build: tle (default), local, install; test-*; clean, help +.github/workflows/test.yml # CI: all three install paths on PG 14-18 create_pgtle_scripts.sh # Vendored pg_tle helper (from github.com/aws/pg_tle) README.md # User-facing documentation AGENTS.md # This file @@ -32,7 +33,7 @@ AGENTS.md # This file | Function | Purpose | |----------|---------| | `to_toon(anyelement, delim)` | Generic encoder: scalars, arrays, records → TOON | -| `row_to_toon(record, delim)` | Record → TOON object (`key: value` lines) | +| `row_to_toon(anyelement, delim)` | Record → TOON object (`key: value` lines) | | `toon_agg(anyelement [, delim])` | Aggregate → TOON tabular array with header + rows | ### Internal helpers (not intended for direct use) @@ -43,6 +44,9 @@ AGENTS.md # This file | `toon_quote_key(text)` | §7.3 key quoting | | `toon_quote_value(text, delim)` | §7.2 value quoting | | `toon_encode_field(raw_json, text_val, delim)` | Type-aware field encoding | +| `toon_agg_sfunc(state, rec, delim)` | `toon_agg` state transition | +| `toon_agg_sfunc_default(state, rec)` | State transition for the 1-arg `toon_agg` (comma delimiter) | +| `toon_agg_ffunc(state)` | `toon_agg` final function (header + rows) | ### Type @@ -61,6 +65,17 @@ AGENTS.md # This file (→ null per §3) from the string literal "NaN" (→ normal string). PG wraps float NaN as a JSON string `"NaN"`, making them otherwise indistinguishable. +## Supported Versions + +PostgreSQL 14 and newer. Versions 12 and 13 are past upstream end-of-life and +are not tested; don't add workarounds for them. When a new PostgreSQL major is +released, add it to the matrix in `.github/workflows/test.yml`; when one goes +EOL, drop it from the matrix and bump the floor here and in README.md. + +CI builds pg_tle from a pinned commit (`PG_TLE_REF` in the workflow) because +no tagged pg_tle release supports PG 18 yet. Switch to a release tag once one +does. + ## Security Model Every function has: @@ -90,7 +105,12 @@ cause a parse error. Always use one of the three paths above. ### Running tests ```sh -# Against a standalone install: +# Make targets (recreate $TESTDB, fail non-zero on any assertion failure): +make test # standalone build +make test-tle # pg_tle install; run BEFORE make install +make install && make test-extension + +# Or by hand, against a standalone install: make local psql -f pgtoon-local.sql psql -c "SET search_path = toon, pg_catalog, pg_temp" -f test_pgtoon.sql @@ -104,11 +124,12 @@ psql -f test_pgtoon.sql # functions are in public by default Tests use a temp table + `assert_toon(name, actual, expected)` helper. Output is a summary row: `passed | failed | total`. Any failures also print -the test name with expected vs actual values via `RAISE NOTICE`. +the test name with expected vs actual values via `RAISE NOTICE`, and a final +`DO` block raises an exception so psql exits non-zero (this is what CI keys on). ### Test requirements -- PostgreSQL 12+ (tested on 16 and 18) +- PostgreSQL 14+ (the supported floor; CI runs 14, 15, 16, 17 and 18) - The extension must be installed before running tests - Tests are self-contained (CREATE/DROP their own temp tables) @@ -134,13 +155,14 @@ Imperative mood, max 50-char subject. Body explains what/why. 2. Add `SET search_path = pg_catalog, pg_temp` 3. Qualify any calls to other pgtoon functions with `@extschema@.` 4. Add tests in `test_pgtoon.sql` -5. Verify all three install paths work (`make tle`, `make local`, filesystem) -6. Run the regression suite: expect 0 failures +5. Run the suite on all three install paths: `make test`, `make test-tle`, + `make install && make test-extension` — expect 0 failures ### Before committing -- Run `make local && psql -f pgtoon-local.sql && psql -f test_pgtoon.sql` (all tests pass) -- Ideally test `make tle` + `CREATE EXTENSION` on a real pg_tle install +- Run `make test` (all tests pass) +- Ideally also `make test-tle` and `make install && make test-extension`; + CI runs all three on PostgreSQL 14–18 for every push and PR ## TOON Spec Quick Reference (for encoders) diff --git a/Makefile b/Makefile index db84444..4481dda 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,13 @@ SRC = $(EXTENSION)--$(EXTVERSION).sql # Schema used by `make local` (the standalone, non-extension build). SCHEMA ?= toon -.PHONY: tle local clean help +PG_CONFIG ?= pg_config +PSQL ?= psql -X -v ON_ERROR_STOP=1 +# Scratch database used by the test targets; dropped and recreated each run. +TESTDB ?= pgtoon_test +EXTDIR = $(shell $(PG_CONFIG) --sharedir)/extension + +.PHONY: tle local install uninstall test test-local test-extension test-tle clean help # Default target: build the pg_tle installable script. tle: .pgtle-$(EXTENSION).sql @@ -33,6 +39,42 @@ $(EXTENSION)-local.sql: $(SRC) sed 's/@extschema@/$(SCHEMA)/g' $(SRC) ) > $@ @echo "Built $@ — load with: psql -f $@" +# Install the control file and SQL script for `CREATE EXTENSION pgtoon`. +install: + install -d '$(DESTDIR)$(EXTDIR)' + install -m 644 $(EXTENSION).control $(SRC) '$(DESTDIR)$(EXTDIR)/' + +uninstall: + rm -f '$(DESTDIR)$(EXTDIR)/$(EXTENSION).control' '$(DESTDIR)$(EXTDIR)/$(SRC)' + +# Test targets connect using the standard libpq env vars (PGHOST, PGUSER, ...). +# Each one recreates $(TESTDB) and runs test_pgtoon.sql against one install path. +define fresh_testdb + $(PSQL) -d postgres -c 'DROP DATABASE IF EXISTS $(TESTDB)' -c 'CREATE DATABASE $(TESTDB)' +endef + +test: test-local + +# Standalone build (sed-substituted schema). +test-local: local + $(fresh_testdb) + $(PSQL) -d $(TESTDB) -f $(EXTENSION)-local.sql + $(PSQL) -d $(TESTDB) -c 'SET search_path = $(SCHEMA), pg_catalog, pg_temp' -f test_pgtoon.sql + +# Filesystem extension (requires `make install`), default and explicit schema. +test-extension: + $(fresh_testdb) + $(PSQL) -d $(TESTDB) -c 'CREATE EXTENSION $(EXTENSION)' -f test_pgtoon.sql + $(fresh_testdb) + $(PSQL) -d $(TESTDB) -c 'CREATE SCHEMA ext' -c 'CREATE EXTENSION $(EXTENSION) SCHEMA ext' \ + -c 'SET search_path = ext, public' -f test_pgtoon.sql + +# pg_tle install (requires pg_tle in shared_preload_libraries). +test-tle: tle + $(fresh_testdb) + $(PSQL) -d $(TESTDB) -f .pgtle-$(EXTENSION).sql + $(PSQL) -d $(TESTDB) -c 'CREATE EXTENSION $(EXTENSION)' -f test_pgtoon.sql + clean: rm -f .pgtle-$(EXTENSION).sql $(EXTENSION)-local.sql @@ -40,4 +82,8 @@ help: @echo "Targets:" @echo " make tle - build pg_tle install script (.pgtle-$(EXTENSION).sql) [default]" @echo " make local - build standalone script ($(EXTENSION)-local.sql), SCHEMA=$(SCHEMA)" + @echo " make install - install control + SQL into \`$(PG_CONFIG) --sharedir\`/extension" + @echo " make test - run the suite against a standalone build (alias: test-local)" + @echo " make test-extension - run the suite via CREATE EXTENSION (needs make install)" + @echo " make test-tle - run the suite via pg_tle (needs pg_tle preloaded)" @echo " make clean - remove generated files" diff --git a/README.md b/README.md index 6bd61f7..2eb1c5b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ vs. the equivalent JSON (96 bytes larger): ## Installation -Requires PostgreSQL 12+. +Requires PostgreSQL 14+. The canonical source `pgtoon--0.1.sql` contains `@extschema@` markers and a locked `search_path`, so it is installed as a PostgreSQL **extension** (the @@ -179,6 +179,23 @@ psql -c "SET search_path = toon, pg_catalog, pg_temp" -f test_pgtoon.sql Or against a `CREATE EXTENSION` install, with the extension's schema on `search_path`. +The Makefile wraps each install path in a test target. Each one recreates a +scratch database (`TESTDB`, default `pgtoon_test`), connects using the usual +libpq environment variables (`PGHOST`, `PGPORT`, `PGUSER`), and exits non-zero +if any assertion fails: + +```sh +make test # standalone build (alias for test-local) +make test-tle # via pg_tle (pg_tle must be in shared_preload_libraries) +make install # copy control + SQL into `pg_config --sharedir`/extension +make test-extension # via CREATE EXTENSION, default schema and SCHEMA ext +``` + +`test-tle` fails if a filesystem copy is installed (pg_tle won't register an +extension that already exists on disk), so run it before `make install`. + +CI (`.github/workflows/test.yml`) runs all three on PostgreSQL 14–18. + The test suite validates key quoting, value quoting, object encoding, tabular array encoding, null handling, NaN/Infinity normalization, and delimiter variants. ## Future Work diff --git a/test_pgtoon.sql b/test_pgtoon.sql index 0d123bf..34bf682 100644 --- a/test_pgtoon.sql +++ b/test_pgtoon.sql @@ -21,7 +21,7 @@ CREATE TEMP TABLE test_results ( CREATE OR REPLACE FUNCTION assert_toon(test_name text, actual text, expected text) RETURNS void LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO test_results VALUES (test_name, actual = expected, expected, actual); + INSERT INTO test_results VALUES (test_name, actual IS NOT DISTINCT FROM expected, expected, actual); IF actual IS DISTINCT FROM expected THEN RAISE NOTICE 'FAIL: % — expected [%], got [%]', test_name, expected, actual; END IF; @@ -337,6 +337,18 @@ FROM test_results WHERE NOT passed ORDER BY test_name; +-- Fail the run (non-zero psql exit under ON_ERROR_STOP) if any assertion failed +DO $$ +DECLARE + n_failed int; +BEGIN + SELECT count(*) INTO n_failed FROM test_results WHERE NOT passed; + IF n_failed > 0 THEN + RAISE EXCEPTION '% test(s) failed', n_failed; + END IF; +END; +$$; + -- Cleanup DROP TABLE test_results; DROP TABLE users;