Skip to content
Merged
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
74 changes: 74 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
40 changes: 31 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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)

Expand Down
48 changes: 47 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,11 +39,51 @@ $(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

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"
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion test_pgtoon.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading