Skip to content

Commit 8fd907f

Browse files
authored
Merge pull request #3 from constructive-io/devin/1769048761-pgpm-integration
feat: Add seed.pgpm() adapter and pgpm integration tests
2 parents 33f4c68 + 3688b00 commit 8fd907f

12 files changed

Lines changed: 384 additions & 1 deletion

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 -s --log-cli-level=INFO

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ jobs:
6565
run: poetry run mypy src --ignore-missing-imports
6666

6767
- name: Run tests
68-
run: poetry run pytest -v
68+
run: poetry run pytest -v --ignore=tests/test_pgpm_integration.py

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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
package: str | None = None,
39+
deploy_args: list[str] | None = None,
40+
cache: bool = False,
41+
) -> None:
42+
"""
43+
Initialize the pgpm seed adapter.
44+
45+
Args:
46+
module_path: Path to the pgpm module directory (defaults to cwd)
47+
package: Package name to deploy (avoids interactive prompt)
48+
deploy_args: Additional arguments to pass to pgpm deploy
49+
cache: Whether to enable caching (not yet implemented)
50+
"""
51+
self._module_path = module_path
52+
self._package = package
53+
self._deploy_args = deploy_args or []
54+
self._cache = cache
55+
56+
def seed(self, ctx: SeedContext) -> None:
57+
"""
58+
Run pgpm deploy to apply migrations.
59+
60+
Args:
61+
ctx: Seed context containing pg client and config
62+
63+
Raises:
64+
RuntimeError: If pgpm deploy fails
65+
"""
66+
config = ctx["config"]
67+
68+
# Build environment with database connection info
69+
env = os.environ.copy()
70+
env["PGHOST"] = config.get("host", "localhost")
71+
env["PGPORT"] = str(config.get("port", 5432))
72+
env["PGDATABASE"] = config["database"]
73+
env["PGUSER"] = config.get("user", "postgres")
74+
if "password" in config:
75+
env["PGPASSWORD"] = config["password"]
76+
77+
# Determine working directory
78+
cwd = self._module_path or os.getcwd()
79+
80+
# Build pgpm deploy command
81+
cmd = ["pgpm", "deploy", "--yes", "--verbose"]
82+
if self._package:
83+
cmd.extend(["--package", self._package])
84+
cmd.extend(self._deploy_args)
85+
86+
logger.info(f"Running pgpm deploy in {cwd}")
87+
logger.debug(f"Command: {' '.join(cmd)}")
88+
logger.debug(f"Database: {config['database']}")
89+
90+
try:
91+
result = subprocess.run(
92+
cmd,
93+
cwd=cwd,
94+
env=env,
95+
capture_output=True,
96+
text=True,
97+
check=False,
98+
)
99+
100+
if result.returncode != 0:
101+
error_msg = result.stderr or result.stdout or "Unknown error"
102+
logger.error(f"pgpm deploy failed: {error_msg}")
103+
raise RuntimeError(f"pgpm deploy failed: {error_msg}")
104+
105+
logger.info("pgpm deploy completed successfully")
106+
if result.stdout:
107+
logger.info(f"pgpm output: {result.stdout}")
108+
if result.stderr:
109+
logger.info(f"pgpm stderr: {result.stderr}")
110+
111+
except FileNotFoundError as err:
112+
raise RuntimeError(
113+
"pgpm not found. Install it with: npm install -g pgpm"
114+
) from err
115+
116+
117+
def pgpm(
118+
module_path: str | None = None,
119+
package: str | None = None,
120+
deploy_args: list[str] | None = None,
121+
cache: bool = False,
122+
) -> PgpmSeedAdapter:
123+
"""
124+
Create a pgpm seed adapter.
125+
126+
This adapter runs pgpm deploy to apply database migrations as part of
127+
test seeding. Requires pgpm to be installed globally.
128+
129+
Args:
130+
module_path: Path to the pgpm module directory (defaults to cwd)
131+
package: Package name to deploy (avoids interactive prompt)
132+
deploy_args: Additional arguments to pass to pgpm deploy
133+
cache: Whether to enable caching
134+
135+
Returns:
136+
A PgpmSeedAdapter instance
137+
138+
Example:
139+
# Deploy migrations from a specific module
140+
seed_adapters = [
141+
seed.pgpm(module_path="./packages/my-module", package="my-module")
142+
]
143+
144+
# Deploy with additional arguments
145+
seed_adapters = [
146+
seed.pgpm(module_path="./my-module", package="my-module", deploy_args=["--verbose"])
147+
]
148+
"""
149+
return PgpmSeedAdapter(module_path=module_path, package=package, 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;

0 commit comments

Comments
 (0)