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
89 changes: 89 additions & 0 deletions .github/workflows/test-pgpm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: pgpm Integration Tests

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-pgpm-tests
cancel-in-progress: true

env:
PGPM_VERSION: '2.7.9'

jobs:
test-pgpm:
runs-on: ubuntu-latest

env:
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
PGPASSWORD: password

services:
pg_db:
image: ghcr.io/constructive-io/docker/postgres-plus:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Cache pgpm CLI
uses: actions/cache@v4
with:
path: ~/.npm
key: pgpm-${{ runner.os }}-${{ env.PGPM_VERSION }}

- name: Install pgpm CLI globally
run: npm install -g pgpm@${{ env.PGPM_VERSION }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install Python dependencies
run: poetry install

- name: Install @pgpm/faker in test fixture
run: |
cd tests/fixtures/pgpm-workspace/packages/test-module
pgpm install @pgpm/faker

- name: Seed pg and app_user
run: |
pgpm admin-users bootstrap --yes
pgpm admin-users add --test --yes

- name: Run pgpm integration tests
run: poetry run pytest tests/test_pgpm_integration.py -v -s --log-cli-level=INFO
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ jobs:
run: poetry run mypy src --ignore-missing-imports

- name: Run tests
run: poetry run pytest -v
run: poetry run pytest -v --ignore=tests/test_pgpm_integration.py
3 changes: 3 additions & 0 deletions src/pysql_test/seed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
- sqlfile: Execute raw SQL files
- fn: Run custom Python functions
- compose: Combine multiple adapters
- pgpm: Run pgpm migrations (requires pgpm CLI)
"""

from pysql_test.seed.adapters import compose, fn
from pysql_test.seed.pgpm import pgpm
from pysql_test.seed.sql import sqlfile

__all__ = [
"sqlfile",
"fn",
"compose",
"pgpm",
]
149 changes: 149 additions & 0 deletions src/pysql_test/seed/pgpm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
pgpm seed adapter for pysql-test.

Provides integration with pgpm (PostgreSQL Package Manager) for running
database migrations as part of test seeding.

Requires pgpm to be installed globally: npm install -g pgpm
"""

from __future__ import annotations

import logging
import os
import subprocess
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pysql_test.types import SeedContext

logger = logging.getLogger(__name__)


class PgpmSeedAdapter:
"""
Seed adapter that runs pgpm deploy to apply migrations.

This adapter calls the pgpm CLI via subprocess, passing the database
connection info via environment variables.

Usage:
adapter = PgpmSeedAdapter(module_path="./my-module")
adapter.seed(ctx)
"""

def __init__(
self,
module_path: str | None = None,
package: str | None = None,
deploy_args: list[str] | None = None,
cache: bool = False,
) -> None:
"""
Initialize the pgpm seed adapter.

Args:
module_path: Path to the pgpm module directory (defaults to cwd)
package: Package name to deploy (avoids interactive prompt)
deploy_args: Additional arguments to pass to pgpm deploy
cache: Whether to enable caching (not yet implemented)
"""
self._module_path = module_path
self._package = package
self._deploy_args = deploy_args or []
self._cache = cache

def seed(self, ctx: SeedContext) -> None:
"""
Run pgpm deploy to apply migrations.

Args:
ctx: Seed context containing pg client and config

Raises:
RuntimeError: If pgpm deploy fails
"""
config = ctx["config"]

# Build environment with database connection info
env = os.environ.copy()
env["PGHOST"] = config.get("host", "localhost")
env["PGPORT"] = str(config.get("port", 5432))
env["PGDATABASE"] = config["database"]
env["PGUSER"] = config.get("user", "postgres")
if "password" in config:
env["PGPASSWORD"] = config["password"]

# Determine working directory
cwd = self._module_path or os.getcwd()

# Build pgpm deploy command
cmd = ["pgpm", "deploy", "--yes", "--verbose"]
if self._package:
cmd.extend(["--package", self._package])
cmd.extend(self._deploy_args)

logger.info(f"Running pgpm deploy in {cwd}")
logger.debug(f"Command: {' '.join(cmd)}")
logger.debug(f"Database: {config['database']}")

try:
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
capture_output=True,
text=True,
check=False,
)

if result.returncode != 0:
error_msg = result.stderr or result.stdout or "Unknown error"
logger.error(f"pgpm deploy failed: {error_msg}")
raise RuntimeError(f"pgpm deploy failed: {error_msg}")

logger.info("pgpm deploy completed successfully")
if result.stdout:
logger.info(f"pgpm output: {result.stdout}")
if result.stderr:
logger.info(f"pgpm stderr: {result.stderr}")

except FileNotFoundError as err:
raise RuntimeError(
"pgpm not found. Install it with: npm install -g pgpm"
) from err


def pgpm(
module_path: str | None = None,
package: str | None = None,
deploy_args: list[str] | None = None,
cache: bool = False,
) -> PgpmSeedAdapter:
"""
Create a pgpm seed adapter.

This adapter runs pgpm deploy to apply database migrations as part of
test seeding. Requires pgpm to be installed globally.

Args:
module_path: Path to the pgpm module directory (defaults to cwd)
package: Package name to deploy (avoids interactive prompt)
deploy_args: Additional arguments to pass to pgpm deploy
cache: Whether to enable caching

Returns:
A PgpmSeedAdapter instance

Example:
# Deploy migrations from a specific module
seed_adapters = [
seed.pgpm(module_path="./packages/my-module", package="my-module")
]

# Deploy with additional arguments
seed_adapters = [
seed.pgpm(module_path="./my-module", package="my-module", deploy_args=["--verbose"])
]
"""
return PgpmSeedAdapter(module_path=module_path, package=package, deploy_args=deploy_args, cache=cache)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Deploy test-module:schemas/test_app to pg

BEGIN;

CREATE SCHEMA test_app;

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "test-module",
"version": "0.0.1",
"description": "Test module for pysql-test pgpm integration",
"author": "pysql-test",
"license": "MIT"
}
5 changes: 5 additions & 0 deletions tests/fixtures/pgpm-workspace/packages/test-module/pgpm.plan
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
%syntax-version=1.0.0
%project=test-module
%uri=test-module

schemas/test_app 2026-01-22T00:00:00Z pysql-test <test@pysql-test.dev> # add test_app schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert test-module:schemas/test_app from pg

BEGIN;

DROP SCHEMA IF EXISTS test_app CASCADE;

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# test-module extension
comment = 'test-module extension for pysql-test'
default_version = '0.0.1'
module_pathname = '$libdir/test-module'
requires = 'plpgsql'
relocatable = false
superuser = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Verify test-module:schemas/test_app on pg

BEGIN;

SELECT pg_catalog.has_schema_privilege('test_app', 'usage');

ROLLBACK;
5 changes: 5 additions & 0 deletions tests/fixtures/pgpm-workspace/pgpm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"packages": [
"packages/*"
]
}
Loading