{"id":43428494,"url":"https://github.com/constructive-io/pgsql-test-python","last_synced_at":"2026-02-02T19:12:46.920Z","repository":{"id":333972490,"uuid":"1138997650","full_name":"constructive-io/pgsql-test-python","owner":"constructive-io","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-22T05:03:09.000Z","size":112,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-22T16:50:26.189Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/constructive-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-21T11:41:41.000Z","updated_at":"2026-01-22T05:03:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/constructive-io/pgsql-test-python","commit_stats":null,"previous_names":["constructive-io/pgsql-test-python"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/constructive-io/pgsql-test-python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/constructive-io%2Fpgsql-test-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/constructive-io%2Fpgsql-test-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/constructive-io%2Fpgsql-test-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/constructive-io%2Fpgsql-test-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/constructive-io","download_url":"https://codeload.github.com/constructive-io/pgsql-test-python/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/constructive-io%2Fpgsql-test-python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29017941,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-02T18:51:31.335Z","status":"ssl_error","status_checked_at":"2026-02-02T18:49:20.777Z","response_time":58,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-02T19:12:46.332Z","updated_at":"2026-02-02T19:12:46.914Z","avatar_url":"https://github.com/constructive-io.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pgsql-test\n\n\u003cp align=\"center\" width=\"100%\"\u003e\n  \u003cimg height=\"250\" src=\"https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\" width=\"100%\"\u003e\n  \u003ca href=\"https://github.com/constructive-io/pgsql-test-python/actions/workflows/test.yml\"\u003e\n    \u003cimg height=\"20\" src=\"https://github.com/constructive-io/pgsql-test-python/actions/workflows/test.yml/badge.svg\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/constructive-io/pgsql-test-python/blob/main/LICENSE\"\u003e\n    \u003cimg height=\"20\" src=\"https://img.shields.io/badge/license-MIT-blue.svg\"/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/pgsql-test\"\u003e\n    \u003cimg height=\"20\" src=\"https://img.shields.io/npm/v/pgsql-test.svg\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nThe Python counterpart to [`pgsql-test`](https://www.npmjs.com/package/pgsql-test) on npm. Instant, isolated PostgreSQL databases for each test — with automatic transaction rollbacks, context switching, and clean seeding.\n\n\u003e **New to pgpm?** Check out the [Workspace Setup Guide](https://github.com/constructive-io/pgsql-test-python/blob/main/WORKSPACE_SETUP.md) for a complete walkthrough of creating a pgpm workspace with Python tests.\n\n## Features\n\n* **Instant test DBs** — each one seeded, isolated, and UUID-named\n* **Per-test rollback** — every test runs in its own transaction with savepoint-based rollback via `before_each()`/`after_each()`\n* **RLS-friendly** — test with role-based auth via `set_context()`\n* **pgpm integration** — run database migrations using [pgpm](https://pgpm.io) (PostgreSQL Package Manager)\n* **Flexible seeding** — run `.sql` files, programmatic seeds, pgpm modules, or combine multiple strategies\n* **Auto teardown** — no residue, no reboots, just clean exits\n\n## Installation\n\n```bash\n# Using Poetry (recommended)\npoetry add pgsql-test\n\n# Using pip\npip install pgsql-test\n```\n\n## Quick Start\n\n```python\nimport pytest\nfrom pgsql_test import get_connections, seed\n\n# Basic usage\ndef test_basic_query():\n    conn = get_connections()\n    result = conn.db.query('SELECT 1 as value')\n    assert result.rows[0]['value'] == 1\n    conn.teardown()\n\n# With pytest fixture\n@pytest.fixture\ndef db():\n    conn = get_connections()\n    yield conn.db\n    conn.teardown()\n\ndef test_with_fixture(db):\n    result = db.query('SELECT 1 as value')\n    assert result.rows[0]['value'] == 1\n```\n\n## pgpm Integration\n\nThe primary use case for pgsql-test is testing PostgreSQL modules managed by [pgpm](https://pgpm.io). The `seed.pgpm()` adapter runs `pgpm deploy` to apply your migrations to an isolated test database.\n\n### Prerequisites\n\nInstall pgpm globally:\n\n```bash\nnpm install -g pgpm\n```\n\n### Basic pgpm Usage\n\n```python\nimport pytest\nfrom pgsql_test import get_connections, seed\n\n@pytest.fixture\ndef db():\n    conn = get_connections(\n        seed_adapters=[\n            seed.pgpm(\n                module_path=\"./packages/my-module\",\n                package=\"my-module\"\n            )\n        ]\n    )\n    db = conn.db\n    db.before_each()\n    yield db\n    db.after_each()\n    conn.teardown()\n\ndef test_my_function(db):\n    # Your pgpm module's functions are now available\n    result = db.one(\"SELECT my_schema.my_function() as result\")\n    assert result['result'] == expected_value\n```\n\n### pgpm with Dependencies\n\nIf your module depends on other pgpm packages (like `@pgpm/faker`), install them first:\n\n```bash\ncd packages/my-module\npgpm install @pgpm/faker\n```\n\nThen test:\n\n```python\ndef test_faker_integration(db):\n    # @pgpm/faker functions are available after pgpm deploy\n    result = db.one(\"SELECT faker.city('MI') as city\")\n    assert result['city'] is not None\n```\n\n### pgpm Workspace Structure\n\nA typical pgpm workspace for testing looks like:\n\n```\nmy-workspace/\n  pgpm.json                    # Workspace config\n  packages/\n    my-module/\n      package.json             # Module metadata\n      my-module.control        # PostgreSQL extension control\n      pgpm.plan                # Migration plan\n      deploy/\n        schemas/\n          my_schema.sql        # CREATE SCHEMA my_schema;\n        functions/\n          my_function.sql      # CREATE FUNCTION ...\n      revert/\n        schemas/\n          my_schema.sql        # DROP SCHEMA my_schema;\n      verify/\n        schemas/\n          my_schema.sql        # SELECT 1 FROM ...\n```\n\n### seed.pgpm() Parameters\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `module_path` | `str` | Path to the pgpm module directory |\n| `package` | `str` | Package name to deploy (required to avoid interactive prompts) |\n| `deploy_args` | `list[str]` | Additional arguments to pass to `pgpm deploy` |\n| `cache` | `bool` | Enable caching (not yet implemented) |\n\n## SQL File Seeding\n\nFor simpler use cases without pgpm, seed directly from SQL files:\n\n```python\n@pytest.fixture\ndef seeded_db():\n    conn = get_connections(\n        seed_adapters=[seed.sqlfile(['schema.sql', 'fixtures.sql'])]\n    )\n    yield conn.db\n    conn.teardown()\n\ndef test_with_seeding(seeded_db):\n    users = seeded_db.many('SELECT * FROM users')\n    assert len(users) \u003e 0\n```\n\n## Per-Test Rollback\n\nThe `before_each()` and `after_each()` methods provide automatic transaction rollback for each test. This ensures complete isolation between tests - any changes made during a test are automatically rolled back, so each test starts with a clean slate.\n\n### How It Works\n\n1. `before_each()` begins a transaction and creates a savepoint\n2. Your test runs and makes changes to the database\n3. `after_each()` rolls back to the savepoint, undoing all changes\n4. The next test starts fresh with only the seeded data\n\n### Basic Pattern\n\n```python\n@pytest.fixture\ndef db():\n    conn = get_connections(\n        seed_adapters=[seed.sqlfile(['schema.sql'])]\n    )\n    db = conn.db\n    db.before_each()  # Begin transaction + savepoint\n    yield db\n    db.after_each()   # Rollback to savepoint\n    conn.teardown()\n\ndef test_insert_user(db):\n    # This insert will be rolled back after the test\n    db.execute(\"INSERT INTO users (name) VALUES ('Test User')\")\n    result = db.one(\"SELECT * FROM users WHERE name = 'Test User'\")\n    assert result['name'] == 'Test User'\n\ndef test_user_count(db):\n    # Previous test's insert is not visible here\n    result = db.one(\"SELECT COUNT(*) as count FROM users\")\n    assert result['count'] == 0  # Only seeded data\n```\n\n### Why This Matters\n\nWithout per-test rollback, tests can interfere with each other:\n- Test A inserts a user\n- Test B expects 0 users but finds 1\n- Tests become order-dependent and flaky\n\nWith `before_each()`/`after_each()`, each test is completely isolated, making your test suite reliable and deterministic.\n\n## RLS Testing\n\nTest Row Level Security policies by switching contexts:\n\n```python\ndef test_rls_policy(db):\n    db.before_each()\n    \n    # Set the user context\n    db.set_context({'app.user_id': '123'})\n    \n    # Now queries will be filtered by RLS policies\n    result = db.many('SELECT * FROM user_data')\n    \n    db.after_each()\n```\n\n## Seeding Strategies\n\n### pgpm Modules\n\n```python\nseed.pgpm(module_path=\"./packages/my-module\", package=\"my-module\")\n```\n\n### SQL Files\n\n```python\nseed.sqlfile(['schema.sql', 'fixtures.sql'])\n```\n\n### Custom Functions\n\n```python\nseed.fn(lambda ctx: ctx['pg'].execute(\n    \"INSERT INTO users (name) VALUES (%s)\", ('Alice',)\n))\n```\n\n### Composed Seeding\n\n```python\nseed.compose([\n    seed.pgpm(module_path=\"./packages/my-module\", package=\"my-module\"),\n    seed.sqlfile(['fixtures.sql']),\n    seed.fn(lambda ctx: ctx['pg'].execute(\"INSERT INTO ...\")),\n])\n```\n\n## Configuration\n\nConfigure via environment variables:\n\n```bash\nexport PGHOST=localhost\nexport PGPORT=5432\nexport PGUSER=postgres\nexport PGPASSWORD=your_password\n```\n\nOr pass configuration directly:\n\n```python\nconn = get_connections(\n    pg_config={\n        'host': 'localhost',\n        'port': 5432,\n        'user': 'postgres',\n        'password': 'your_password',\n    }\n)\n```\n\n## API Reference\n\n### `get_connections(pg_config?, connection_options?, seed_adapters?)`\n\nCreates a new isolated test database and returns connection objects.\n\nReturns a `ConnectionResult` with:\n- `pg`: PgTestClient connected as superuser\n- `db`: PgTestClient for testing (same as pg for now)\n- `admin`: DbAdmin for database management\n- `manager`: PgTestConnector managing connections\n- `teardown()`: Function to clean up\n\n### `PgTestClient`\n\n- `query(sql, params?)`: Execute SQL and return QueryResult\n- `one(sql, params?)`: Return exactly one row\n- `one_or_none(sql, params?)`: Return one row or None\n- `many(sql, params?)`: Return multiple rows\n- `many_or_none(sql, params?)`: Return rows (may be empty)\n- `execute(sql, params?)`: Execute and return affected row count\n- `before_each()`: Start test isolation (transaction + savepoint)\n- `after_each()`: End test isolation (rollback)\n- `set_context(dict)`: Set session variables for RLS testing\n\n## GitHub Actions Example\n\nHere's a complete CI workflow for testing pgpm modules:\n\n```yaml\nname: Test\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    \n    services:\n      postgres:\n        image: postgres:17\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: password\n        options: \u003e-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n\n    env:\n      PGHOST: localhost\n      PGPORT: 5432\n      PGUSER: postgres\n      PGPASSWORD: password\n\n    steps:\n      - uses: actions/checkout@v4\n      \n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n      \n      - name: Install pgpm\n        run: npm install -g pgpm\n      \n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      \n      - name: Install Poetry\n        uses: snok/install-poetry@v1\n      \n      - name: Install dependencies\n        run: poetry install\n      \n      - name: Bootstrap pgpm roles\n        run: |\n          pgpm admin-users bootstrap --yes\n          pgpm admin-users add --test --yes\n      \n      - name: Run tests\n        run: poetry run pytest -v\n```\n\n## Development\n\n```bash\n# Install dependencies\npoetry install\n\n# Run tests\npoetry run pytest\n\n# Run linting\npoetry run ruff check .\n\n# Run type checking\npoetry run mypy src\n```\n\n## Related Projects\n\n- [pgsql-test](https://www.npmjs.com/package/pgsql-test) - The original TypeScript/Node.js version\n- [pgpm](https://pgpm.io) - PostgreSQL Package Manager\n\n## License\n\nMIT\n\n## Credits\n\n**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**\n\n## Disclaimer\n\nAS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED \"AS IS\", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.\n\nNo developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconstructive-io%2Fpgsql-test-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fconstructive-io%2Fpgsql-test-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconstructive-io%2Fpgsql-test-python/lists"}