Metadata-Version: 2.5
Name: alembic-pg-autogen
Version: 0.2.0
Summary: Alembic autogenerate extension for PostgreSQL-specific objects
Project-URL: Documentation, https://alembic-pg-autogen.readthedocs.io
Project-URL: Repository, https://github.com/eddieland/alembic-pg-autogen
Project-URL: Issues, https://github.com/eddieland/alembic-pg-autogen/issues
Author-email: Edward Jones <edwardrjones97@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: alembic,autogenerate,database,migration,postgresql,sqlalchemy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Database :: Front-Ends
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: alembic>=1.19
Requires-Dist: postgast>=0.0.2
Requires-Dist: sqlalchemy>=2
Provides-Extra: docs
Requires-Dist: furo>=2024.8; extra == 'docs'
Requires-Dist: sphinx-autodoc2>=0.5; extra == 'docs'
Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
Requires-Dist: sphinx-llms-txt>=0.7; extra == 'docs'
Requires-Dist: sphinx>=8; extra == 'docs'
Requires-Dist: sphinxext-opengraph>=0.9; extra == 'docs'
Description-Content-Type: text/markdown

# alembic-pg-autogen

[![PyPI](https://img.shields.io/pypi/v/alembic-pg-autogen)](https://pypi.org/project/alembic-pg-autogen/)
[![Python](https://img.shields.io/pypi/pyversions/alembic-pg-autogen)](https://pypi.org/project/alembic-pg-autogen/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/eddieland/alembic-pg-autogen/ci.yml?label=CI)](https://github.com/eddieland/alembic-pg-autogen/actions/workflows/ci.yml)
[![Coverage](https://codecov.io/gh/eddieland/alembic-pg-autogen/graph/badge.svg)](https://codecov.io/gh/eddieland/alembic-pg-autogen)
[![Docs](https://readthedocs.org/projects/alembic-pg-autogen/badge/?version=latest)](https://alembic-pg-autogen.readthedocs.io)
[![Downloads](https://img.shields.io/pypi/dm/alembic-pg-autogen)](https://pypi.org/project/alembic-pg-autogen/)

> **Status: Beta** — the core pipeline is stable and tested against real PostgreSQL. The API may still evolve before
> 1.0, but the library is suitable for production use.

Alembic autogenerate extension for PostgreSQL functions and triggers. Declare your DDL strings and let
`alembic revision --autogenerate` figure out the `CREATE`, `DROP`, and `CREATE OR REPLACE` for you.

<p align="center">
  <img src="https://raw.githubusercontent.com/eddieland/alembic-pg-autogen/main/docs/logo.png" width="350" alt="alembic-pg-autogen logo"/>
</p>

## Background

[alembic_utils](https://github.com/olirice/alembic_utils) pioneered autogenerate support for PostgreSQL objects and has
been hugely helpful to the community. This project takes a different approach aimed at faster performance on large
schemas with many functions and triggers.

## How it works

You declare your desired functions and triggers as plain DDL strings. At autogenerate time, the extension inspects the
live database catalog, canonicalizes your DDL via a temporary savepoint, diffs current vs. desired state, and emits
migration ops in dependency-safe order.

## Quick example

```python
import alembic_pg_autogen  # noqa: F401  # registers the comparator plugin

PG_FUNCTIONS = [
    """
    CREATE OR REPLACE FUNCTION set_updated_at()
    RETURNS trigger LANGUAGE plpgsql AS $$
    BEGIN
        NEW.updated_at = now();
        RETURN NEW;
    END;
    $$
    """,
]

PG_TRIGGERS = [
    """
    CREATE TRIGGER set_updated_at_on_update
    BEFORE UPDATE ON my_table
    FOR EACH ROW EXECUTE FUNCTION set_updated_at()
    """,
]

# in run_migrations_online():
context.configure(
    connection=connection,
    target_metadata=target_metadata,
    pg_functions=PG_FUNCTIONS,
    pg_triggers=PG_TRIGGERS,
)
```

```bash
alembic revision --autogenerate -m "add audit function and trigger"
```

## What gets managed

An object type becomes *managed* when you declare it. Within a managed type the declared set is the whole truth, so
objects found in the inspected schemas that you did not declare are dropped — that is what makes the tool declarative.

A key you never pass leaves that object type alone entirely: nothing is inspected, diffed, or emitted for it. The
example above declares functions and triggers, so views are untouched, and adopting the library one object type at a
time is safe.

To record the opt-out at the configuration site — or to set it conditionally — pass the `IGNORED` sentinel, which means
exactly what omitting the key means:

```python
from alembic_pg_autogen import IGNORED

context.configure(
    connection=connection,
    target_metadata=target_metadata,
    pg_functions=PG_FUNCTIONS,
    pg_triggers=PG_TRIGGERS,
    pg_views=IGNORED,  # not ready to manage views yet — don't drop them
)
```

An empty list is a different thing again: `pg_views=[]` declares "there should be no views" and drops every existing
one, while `pg_views=IGNORED` declares "views are not managed here". Watch for this if you build the list dynamically —
`collect_view_ddl() or IGNORED` keeps an empty result from clearing your schema.

Because an unrecognized key leaves its type unmanaged, a misspelled one (`pg_view` for `pg_views`) would quietly manage
nothing; those are reported as a warning naming the key you meant.

## Check constraints

Alembic detects when a named `CHECK` constraint is added to or removed from your models, but two constraints that share
a name are always presumed equivalent — normalizing SQL expressions across backends is not something Alembic can do. So
tightening `amount >= 0` to `amount > 0` in a model generates nothing, and the schema drifts.

This package closes that gap for PostgreSQL. Keep declaring constraints in SQLAlchemy metadata as usual:

```python
class Order(Base):
    __tablename__ = "orders"

    id: Mapped[int] = mapped_column(primary_key=True)
    amount: Mapped[Decimal]

    __table_args__ = (CheckConstraint("amount > 0", name="ck_orders_amount"),)
```

and a changed expression now produces a migration:

```python
def upgrade() -> None:
    op.drop_constraint("ck_orders_amount", "orders", type_="check")
    op.create_check_constraint("ck_orders_amount", "orders", "amount > 0")
```

There is nothing to configure. Each expression is round-tripped through PostgreSQL — added to the table as a throwaway
`NOT VALID` constraint inside a savepoint that is rolled back — so `amount >= 0` and the catalog's
`amount >= 0::numeric` are recognized as the same constraint, and a real change is recognized as a real change.

## Installation

```bash
pip install alembic-pg-autogen
```

Requires Python 3.10+ and SQLAlchemy 2.x. Bring your own PostgreSQL driver (`psycopg`, `psycopg2`, `asyncpg`, etc.).
This package depends on [postgast](https://github.com/eddieland/postgast) for DDL parsing, which requires
`protobuf >= 5.27`.

## Documentation

Full documentation is available at [alembic-pg-autogen.readthedocs.io](https://alembic-pg-autogen.readthedocs.io),
including a quick-start guide, migration instructions for alembic_utils users, and API reference.

## Development

```bash
make install     # Install dependencies (uses uv)
make lint        # Format (mdformat, codespell, ruff) then type-check (basedpyright)
make test        # Run full test suite (requires Docker for integration tests)
make test-unit   # Run unit tests only (no Docker needed)
```

## License

[MIT](LICENSE)
