Skip to content

Commit 62e8c02

Browse files
committed
feat: Add seed.pgpm() adapter and pgpm integration tests
- Add PgpmSeedAdapter that calls pgpm CLI via subprocess - Pass database connection info via PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD env vars - Add pre-scaffolded pgpm workspace fixture with test-module - Add test_pgpm_integration.py with tests for pgpm deploy and @pgpm/faker - Add test-pgpm.yml workflow that: - Installs pgpm CLI globally - Installs @pgpm/faker in the test fixture - Runs pgpm admin-users bootstrap - Runs pgpm integration tests This demonstrates Option 1: using pgpm as a CLI tool from Python to run database migrations without rewriting pgpm in Python.
1 parent 33f4c68 commit 62e8c02

11 files changed

Lines changed: 373 additions & 0 deletions

File tree

.github/workflows/test-pgpm.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: pgpm Integration Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- develop
8+
pull_request:
9+
branches:
10+
- main
11+
- develop
12+
workflow_dispatch:
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}-pgpm-tests
16+
cancel-in-progress: true
17+
18+
env:
19+
PGPM_VERSION: '2.7.9'
20+
21+
jobs:
22+
test-pgpm:
23+
runs-on: ubuntu-latest
24+
25+
env:
26+
PGHOST: localhost
27+
PGPORT: 5432
28+
PGUSER: postgres
29+
PGPASSWORD: password
30+
31+
services:
32+
pg_db:
33+
image: ghcr.io/constructive-io/docker/postgres-plus:17
34+
env:
35+
POSTGRES_USER: postgres
36+
POSTGRES_PASSWORD: password
37+
options: >-
38+
--health-cmd pg_isready
39+
--health-interval 10s
40+
--health-timeout 5s
41+
--health-retries 5
42+
ports:
43+
- 5432:5432
44+
45+
steps:
46+
- name: Checkout
47+
uses: actions/checkout@v4
48+
49+
- name: Setup Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: '20'
53+
54+
- name: Cache pgpm CLI
55+
uses: actions/cache@v4
56+
with:
57+
path: ~/.npm
58+
key: pgpm-${{ runner.os }}-${{ env.PGPM_VERSION }}
59+
60+
- name: Install pgpm CLI globally
61+
run: npm install -g pgpm@${{ env.PGPM_VERSION }}
62+
63+
- name: Set up Python
64+
uses: actions/setup-python@v5
65+
with:
66+
python-version: "3.12"
67+
68+
- name: Install Poetry
69+
uses: snok/install-poetry@v1
70+
with:
71+
version: latest
72+
virtualenvs-create: true
73+
virtualenvs-in-project: true
74+
75+
- name: Install Python dependencies
76+
run: poetry install
77+
78+
- name: Install @pgpm/faker in test fixture
79+
run: |
80+
cd tests/fixtures/pgpm-workspace/packages/test-module
81+
pgpm install @pgpm/faker
82+
83+
- name: Seed pg and app_user
84+
run: |
85+
pgpm admin-users bootstrap --yes
86+
pgpm admin-users add --test --yes
87+
88+
- name: Run pgpm integration tests
89+
run: poetry run pytest tests/test_pgpm_integration.py -v

src/pysql_test/seed/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
- sqlfile: Execute raw SQL files
66
- fn: Run custom Python functions
77
- compose: Combine multiple adapters
8+
- pgpm: Run pgpm migrations (requires pgpm CLI)
89
"""
910

1011
from pysql_test.seed.adapters import compose, fn
12+
from pysql_test.seed.pgpm import pgpm
1113
from pysql_test.seed.sql import sqlfile
1214

1315
__all__ = [
1416
"sqlfile",
1517
"fn",
1618
"compose",
19+
"pgpm",
1720
]

src/pysql_test/seed/pgpm.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
pgpm seed adapter for pysql-test.
3+
4+
Provides integration with pgpm (PostgreSQL Package Manager) for running
5+
database migrations as part of test seeding.
6+
7+
Requires pgpm to be installed globally: npm install -g pgpm
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import logging
13+
import os
14+
import subprocess
15+
from typing import TYPE_CHECKING
16+
17+
if TYPE_CHECKING:
18+
from pysql_test.types import SeedContext
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class PgpmSeedAdapter:
24+
"""
25+
Seed adapter that runs pgpm deploy to apply migrations.
26+
27+
This adapter calls the pgpm CLI via subprocess, passing the database
28+
connection info via environment variables.
29+
30+
Usage:
31+
adapter = PgpmSeedAdapter(module_path="./my-module")
32+
adapter.seed(ctx)
33+
"""
34+
35+
def __init__(
36+
self,
37+
module_path: str | None = None,
38+
deploy_args: list[str] | None = None,
39+
cache: bool = False,
40+
) -> None:
41+
"""
42+
Initialize the pgpm seed adapter.
43+
44+
Args:
45+
module_path: Path to the pgpm module directory (defaults to cwd)
46+
deploy_args: Additional arguments to pass to pgpm deploy
47+
cache: Whether to enable caching (not yet implemented)
48+
"""
49+
self._module_path = module_path
50+
self._deploy_args = deploy_args or []
51+
self._cache = cache
52+
53+
def seed(self, ctx: SeedContext) -> None:
54+
"""
55+
Run pgpm deploy to apply migrations.
56+
57+
Args:
58+
ctx: Seed context containing pg client and config
59+
60+
Raises:
61+
RuntimeError: If pgpm deploy fails
62+
"""
63+
config = ctx["config"]
64+
65+
# Build environment with database connection info
66+
env = os.environ.copy()
67+
env["PGHOST"] = config.get("host", "localhost")
68+
env["PGPORT"] = str(config.get("port", 5432))
69+
env["PGDATABASE"] = config["database"]
70+
env["PGUSER"] = config.get("user", "postgres")
71+
if "password" in config:
72+
env["PGPASSWORD"] = config["password"]
73+
74+
# Determine working directory
75+
cwd = self._module_path or os.getcwd()
76+
77+
# Build pgpm deploy command
78+
cmd = ["pgpm", "deploy", "--yes"]
79+
cmd.extend(self._deploy_args)
80+
81+
logger.info(f"Running pgpm deploy in {cwd}")
82+
logger.debug(f"Command: {' '.join(cmd)}")
83+
logger.debug(f"Database: {config['database']}")
84+
85+
try:
86+
result = subprocess.run(
87+
cmd,
88+
cwd=cwd,
89+
env=env,
90+
capture_output=True,
91+
text=True,
92+
check=False,
93+
)
94+
95+
if result.returncode != 0:
96+
error_msg = result.stderr or result.stdout or "Unknown error"
97+
logger.error(f"pgpm deploy failed: {error_msg}")
98+
raise RuntimeError(f"pgpm deploy failed: {error_msg}")
99+
100+
logger.info("pgpm deploy completed successfully")
101+
if result.stdout:
102+
logger.debug(f"pgpm output: {result.stdout}")
103+
104+
except FileNotFoundError as err:
105+
raise RuntimeError(
106+
"pgpm not found. Install it with: npm install -g pgpm"
107+
) from err
108+
109+
110+
def pgpm(
111+
module_path: str | None = None,
112+
deploy_args: list[str] | None = None,
113+
cache: bool = False,
114+
) -> PgpmSeedAdapter:
115+
"""
116+
Create a pgpm seed adapter.
117+
118+
This adapter runs pgpm deploy to apply database migrations as part of
119+
test seeding. Requires pgpm to be installed globally.
120+
121+
Args:
122+
module_path: Path to the pgpm module directory (defaults to cwd)
123+
deploy_args: Additional arguments to pass to pgpm deploy
124+
cache: Whether to enable caching
125+
126+
Returns:
127+
A PgpmSeedAdapter instance
128+
129+
Example:
130+
# Deploy migrations from a specific module
131+
seed_adapters = [
132+
seed.pgpm(module_path="./packages/my-module")
133+
]
134+
135+
# Deploy with additional arguments
136+
seed_adapters = [
137+
seed.pgpm(module_path="./my-module", deploy_args=["--verbose"])
138+
]
139+
"""
140+
return PgpmSeedAdapter(module_path=module_path, deploy_args=deploy_args, cache=cache)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Deploy test-module:schemas/test_app to pg
2+
3+
BEGIN;
4+
5+
CREATE SCHEMA test_app;
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "test-module",
3+
"version": "0.0.1",
4+
"description": "Test module for pysql-test pgpm integration",
5+
"author": "pysql-test",
6+
"license": "MIT"
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
%syntax-version=1.0.0
2+
%project=test-module
3+
%uri=test-module
4+
5+
schemas/test_app 2026-01-22T00:00:00Z pysql-test <test@pysql-test.dev> # add test_app schema
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert test-module:schemas/test_app from pg
2+
3+
BEGIN;
4+
5+
DROP SCHEMA IF EXISTS test_app CASCADE;
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# test-module extension
2+
comment = 'test-module extension for pysql-test'
3+
default_version = '0.0.1'
4+
module_pathname = '$libdir/test-module'
5+
requires = 'plpgsql'
6+
relocatable = false
7+
superuser = false
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Verify test-module:schemas/test_app on pg
2+
3+
BEGIN;
4+
5+
SELECT pg_catalog.has_schema_privilege('test_app', 'usage');
6+
7+
ROLLBACK;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"packages": [
3+
"packages/*"
4+
]
5+
}

0 commit comments

Comments
 (0)