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
68 changes: 43 additions & 25 deletions .github/workflows/wesley-holmes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ jobs:
# Ops emission targets schema wes_ops; create it if missing
PGPASSWORD=postgres psql -h localhost -U postgres -d test -c 'CREATE SCHEMA IF NOT EXISTS wes_ops;'

- name: "📦 Apply Ops SQL (functions only)"
- name: "📦 Apply Ops SQL (views + functions)"
env:
WESLEY_OUT_DIR: out
run: |
Expand All @@ -407,34 +407,29 @@ jobs:
echo "No ops SQL directory found; skipping"
exit 0
fi
files=("$OPS_DIR"/*.fn.sql)
if [ ${#files[@]} -eq 0 ]; then
echo "No ops function SQL files found; nothing to apply"
exit 0
fi
for f in "${files[@]}"; do
if [ ! -s "$f" ]; then
echo "Skipping empty file: $f"
# Apply views first, then functions
for pattern in "*.view.sql" "*.fn.sql"; do
files=("$OPS_DIR"/$pattern)
if [ ${#files[@]} -eq 0 ]; then
echo "No $pattern files found; skipping"
continue
fi
echo "Applying $f (single transaction)..."
PGPASSWORD=postgres psql -1 -v ON_ERROR_STOP=1 -h localhost -U postgres -d test -f "$f"
for f in "${files[@]}"; do
if [ ! -s "$f" ]; then
echo "Skipping empty file: $f"
continue
fi
echo "Applying $f (single transaction)..."
PGPASSWORD=postgres psql -1 -v ON_ERROR_STOP=1 -h localhost -U postgres -d test -f "$f"
done
done

- name: "🌱 Seed minimal data for ops"
run: |
set -euo pipefail
# Seed a couple of products (one published, one not) and run in a single transaction
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres -d test <<'SQL'
BEGIN;
insert into product (id, sku, name, slug, price_cents, stock_quantity, published, created_at)
values (gen_random_uuid(), 'SKU1', 'Alpha', 'alpha', 100, 10, true, now())
on conflict do nothing;
insert into product (id, sku, name, slug, price_cents, stock_quantity, published, created_at)
values (gen_random_uuid(), 'SKU2', 'Beta', 'beta', 200, 0, false, now())
on conflict do nothing;
COMMIT;
SQL
# Reuse the same seed file that docker-compose mounts as 03-seed.sql.
# -1 wraps the file in a single transaction (matches docker-entrypoint behaviour).
PGPASSWORD=postgres psql -1 -v ON_ERROR_STOP=1 -h localhost -U postgres -d test \
-f test/fixtures/postgres/03-seed.sql

- name: "🧪 Generate EXPLAIN JSON snapshots"
env:
Expand All @@ -444,7 +439,9 @@ jobs:
mkdir -p test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain
# EXPLAIN (FORMAT JSON) for function calls
PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "EXPLAIN (FORMAT JSON) SELECT * FROM wes_ops.op_products_by_name('Al%')" > test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain/products_by_name.explain.json
PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "EXPLAIN (FORMAT JSON) SELECT * FROM wes_ops.op_orders_by_user('00000000-0000-0000-0000-000000000000')" > test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain/orders_by_user.explain.json
PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "EXPLAIN (FORMAT JSON) SELECT * FROM wes_ops.op_orders_by_user('00000000-0000-0000-0000-000000000001'::uuid)" > test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain/orders_by_user.explain.json
PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "EXPLAIN (FORMAT JSON) SELECT * FROM wes_ops.op_orders_with_items_by_user('00000000-0000-0000-0000-000000000001'::uuid)" > test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain/orders_with_items_by_user.explain.json
PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "EXPLAIN (FORMAT JSON) SELECT * FROM wes_ops.op_allproducts()" > test/fixtures/examples/${WESLEY_OUT_DIR}/ops/explain/allproducts.explain.json

- name: "⬆️ Upload EXPLAIN snapshots"
uses: actions/upload-artifact@v7
Expand All @@ -461,7 +458,28 @@ jobs:
HAS_PGTAP=$(PGPASSWORD=postgres psql -h localhost -U postgres -d test -t -A -c "SELECT 1 FROM pg_available_extensions WHERE name='pgtap'" || echo '')
if [ "$RUN_PGTAP" = "true" ] && [ "$HAS_PGTAP" = "1" ]; then
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres -d test -c 'CREATE EXTENSION IF NOT EXISTS pgtap;'
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres -d test -f test/fixtures/examples/tests/ops.pgtap.sql
shopt -s nullglob
files=(test/fixtures/examples/tests/*.pgtap.sql)
if [ ${#files[@]} -eq 0 ]; then
echo "No pgTAP test files found"
exit 1
fi
TOTAL_PASS=0; TOTAL_FAIL=0
for f in "${files[@]}"; do
echo "Running $f ..."
OUTPUT=$(PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres -d test -f "$f" 2>&1)
echo "$OUTPUT"
# Parse TAP output: count "not ok" lines (TAP failures don't cause psql to exit non-zero)
FILE_FAIL=$(echo "$OUTPUT" | grep -c '^not ok' || true)
FILE_PASS=$(echo "$OUTPUT" | grep -c '^ok' || true)
TOTAL_PASS=$((TOTAL_PASS + FILE_PASS))
TOTAL_FAIL=$((TOTAL_FAIL + FILE_FAIL))
done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "--- TAP Results: $TOTAL_PASS passed, $TOTAL_FAIL failed ---"
if [ "$TOTAL_FAIL" -gt 0 ]; then
echo "pgTAP assertion failures detected" >&2
exit 1
fi
else
echo "pgtap extension not available; skipping pgTAP tests"
fi
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

### Added

- **pgTAP smoke tests for emitted ops** (#416): Three pgTAP test files replacing
the skeleton `ops.pgtap.sql` — `ops-parameterless-view` (view + zero-arg
function), `ops-parameterized-fn` (ILIKE filter with text param), and
`ops-nested-lateral` (LATERAL join with nested jsonb arrays). CI seed data
expanded with deterministic UUIDs (user, order, order items). CI workflow
updated to apply `*.view.sql` alongside `*.fn.sql` and run all `*.pgtap.sql`
files. EXPLAIN snapshots now cover all 4 ops. TAP output is now parsed for
assertion failures so CI exits non-zero on test regressions. Negative-case
and shape assertions added per CodeRabbit review feedback. CI seed step now
reads `test/fixtures/postgres/03-seed.sql` directly instead of inlining a copy.
- **`graphql` dependency for `@wesley/cli`**: Added `graphql` as a direct
dependency of `@wesley/cli` so that `.graphql` ops compilation works under
pnpm's strict module resolution. Previously the dynamic `import('graphql')`
in `generate.mjs` relied on transitive resolution through `@wesley/core`,
which pnpm disallows.
- **QIR Dialect Abstraction** (`@wesley/core`): Introduced `SqlDialect` abstract
interface and `PostgresDialect` implementation that extracts all
PostgreSQL-specific rendering (jsonb functions, `@>` containment, `ILIKE`,
Expand Down
54 changes: 43 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ services:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# Init scripts run alphabetically: extensions → schema → wes_ops → seed → tests
- ./test/fixtures/postgres/00-extensions.sql:/docker-entrypoint-initdb.d/00-extensions.sql:ro
- ./test/fixtures/examples/out/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
- ./test/fixtures/examples/tests/generated.sql:/docker-entrypoint-initdb.d/02-tests.sql:ro
- ./test/fixtures/postgres/02-wes-ops-schema.sql:/docker-entrypoint-initdb.d/02-wes-ops-schema.sql:ro
- ./test/fixtures/postgres/03-seed.sql:/docker-entrypoint-initdb.d/03-seed.sql:ro
- ./test/fixtures/examples/out/tests.sql:/docker-entrypoint-initdb.d/04-tests.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wesley -d wesley_test"]
interval: 10s
timeout: 5s
retries: 5

# pgTAP test runner
# pgTAP test runner — installs pgTAP into the postgres service, applies ops SQL,
# then runs all *.pgtap.sql test files via psql (matching CI approach).
pgtap:
image: postgres:15-alpine
depends_on:
Expand All @@ -34,16 +38,44 @@ services:
PGDATABASE: wesley_test
volumes:
- ./test/fixtures/examples/tests:/tests:ro
- ./test/fixtures/examples/out/ops:/ops:ro
entrypoint: ["/bin/sh", "-c"]
command: |
"apk add --no-cache build-base perl perl-dev && \
cd /tmp && \
wget https://github.com/theory/pgtap/archive/v1.3.0.tar.gz && \
tar -xzf v1.3.0.tar.gz && \
cd pgtap-1.3.0 && \
make && make install && \
psql -c 'CREATE EXTENSION IF NOT EXISTS pgtap' && \
pg_prove -v /tests/*.sql"
command:
- |
set -e
echo '--- Applying ops SQL (views then functions) ---'
for f in /ops/*.view.sql; do
[ -f "$$f" ] && [ -s "$$f" ] && psql -v ON_ERROR_STOP=1 -f "$$f" && echo "Applied $$f"
done
for f in /ops/*.fn.sql; do
[ -f "$$f" ] && [ -s "$$f" ] && psql -v ON_ERROR_STOP=1 -f "$$f" && echo "Applied $$f"
done
echo '--- Installing pgTAP ---'
apk add --no-cache build-base perl
cd /tmp
wget -q https://github.com/theory/pgtap/archive/v1.3.0.tar.gz
tar -xzf v1.3.0.tar.gz
cd pgtap-1.3.0
make > /dev/null 2>&1
DESTDIR=/tmp/pgtap-out make install > /dev/null 2>&1
# Copy extension files to where the postgres *server* expects them,
# but we can't — we're not in the server container.
# Instead, load pgtap-static.sql directly into the remote DB.
psql -v ON_ERROR_STOP=1 -f sql/pgtap-static.sql
Comment thread
flyingrobots marked this conversation as resolved.
echo '--- Running pgTAP tests ---'
PASS=0; FAIL=0
for f in /tests/*.pgtap.sql; do
echo "==> $$f"
OUTPUT=$$(psql -v ON_ERROR_STOP=1 -f "$$f" 2>&1)
echo "$$OUTPUT"
# Parse TAP output: count "not ok" lines (TAP failures don't cause psql to exit non-zero)
FILE_FAIL=$$(echo "$$OUTPUT" | grep -c '^not ok' || true)
FILE_PASS=$$(echo "$$OUTPUT" | grep -c '^ok' || true)
PASS=$$((PASS + FILE_PASS))
FAIL=$$((FAIL + FILE_FAIL))
done
echo "--- TAP Results: $$PASS passed, $$FAIL failed ---"
[ "$$FAIL" -eq 0 ]

volumes:
postgres_data:
3 changes: 2 additions & 1 deletion packages/wesley-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"chokidar": "^4.0.3",
"commander": "^14.0.2"
"commander": "^14.0.2",
"graphql": "^16.11.0"
},
"keywords": [
"cli",
Expand Down
27 changes: 15 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions test/fixtures/examples/ops/all_products.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query AllProducts {
Comment thread
flyingrobots marked this conversation as resolved.
products(orderBy: [{ name: asc }]) {
id
name
slug
price_cents
}
}
70 changes: 70 additions & 0 deletions test/fixtures/examples/tests/ops-nested-lateral.pgtap.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- pgTAP smoke tests: nested LATERAL join (orders_with_items_by_user.op.json)
-- Tests that a LATERAL join operation emits a function with nested jsonb arrays.

BEGIN;
SELECT plan(7);

-- 1. Schema exists
SELECT has_schema('wes_ops', 'wes_ops schema exists');

-- 2. Function exists with uuid parameter
SELECT has_function(
'wes_ops', 'op_orders_with_items_by_user', ARRAY['uuid'],
'op_orders_with_items_by_user(uuid) function exists'
);

-- 3. Seeded user has 1 order
SELECT is(
(SELECT count(*)::bigint FROM wes_ops.op_orders_with_items_by_user(
'00000000-0000-0000-0000-000000000001'::uuid
)),
1::bigint,
'op_orders_with_items_by_user returns 1 order for seeded user'
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

-- 4. Negative case: non-existent user returns 0 rows (proves user_id predicate works)
SELECT is(
(SELECT count(*)::bigint FROM wes_ops.op_orders_with_items_by_user(
'00000000-0000-0000-0000-000000000002'::uuid
)),
0::bigint,
'op_orders_with_items_by_user returns 0 orders for non-existent user'
);

-- 5. Returned jsonb has expected top-level keys (id, order_number, status, items)
SELECT ok(
(SELECT bool_and(
row_data ? 'id'
AND row_data ? 'order_number'
AND row_data ? 'status'
AND row_data ? 'items'
) FROM wes_ops.op_orders_with_items_by_user(
'00000000-0000-0000-0000-000000000001'::uuid
) AS row_data),
'every row has id, order_number, status, items keys'
);

-- 6. Every order's nested items array has 2 order items
SELECT ok(
(SELECT bool_and(jsonb_array_length(row_data -> 'items') = 2)
FROM wes_ops.op_orders_with_items_by_user(
'00000000-0000-0000-0000-000000000001'::uuid
) AS row_data),
'all orders have exactly 2 items'
);

-- 7. Each nested item has required keys (id, product_id, quantity)
SELECT ok(
(SELECT bool_and(
elem ? 'id'
AND elem ? 'product_id'
AND elem ? 'quantity'
) FROM wes_ops.op_orders_with_items_by_user(
'00000000-0000-0000-0000-000000000001'::uuid
) AS row_data,
LATERAL jsonb_array_elements(row_data -> 'items') AS elem),
'every nested item has id, product_id, quantity keys'
);

SELECT finish();
ROLLBACK;
Loading
Loading