Metadata-Version: 2.4
Name: sqlpush
Version: 0.4.1
Summary: Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models
Keywords: sqlalchemy,alembic,postgresql,timescaledb,prisma,schema,migrations,database,drift,cli
Author: Juan Miguel Contreras
Author-email: Juan Miguel Contreras <19253629+juanmicl@users.noreply.github.com>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
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: Topic :: Database
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Dist: alembic>=1.18,<2
Requires-Dist: psycopg[binary]>=3.2
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: typer>=0.12
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/juanmicl/sqlpush
Project-URL: Repository, https://github.com/juanmicl/sqlpush
Project-URL: Issues, https://github.com/juanmicl/sqlpush/issues
Project-URL: Changelog, https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# sqlpush

[![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
[![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)

**Prisma `db push` for SQLAlchemy.** Apply your models (SQLAlchemy,
SQLModel, anything built on `MetaData`) to a live PostgreSQL / TimescaleDB
database directly, no migration files. sqlpush
diffs your models against the real schema, classifies every operation by
risk (safe / risky / destructive), and applies the plan atomically. Drift
checks exit with codes your CI can gate on.

```console
sqlpush diff "myapp.models:metadata"      # see the SQL, ordered by risk
sqlpush check "myapp.models:metadata"     # CI gate: exit 0/2/3
sqlpush push "myapp.models:metadata"      # apply (destructive gated)
```

If you've ever run `Base.metadata.create_all()` in production and known it
was wrong, then sighed at the migration-script treadmill when you reached
for alembic: sqlpush is for you.

## Why

Declarative models are already the source of truth. Migration files
re-encode what the models say, drift from them, and pile up forever.
sqlpush closes the loop the way Prisma's `db push` does for its schema
language, but for the SQLAlchemy ecosystem (SQLModel included):

- **No migration files, ever.** The diff *is* the migration: computed fresh
  from models vs. live database on every run, via alembic's autogenerate
  engine used as a library.
- **Risk-aware by default.** Every operation is classified `safe` /
  `risky` / `destructive`. Destructive ops (drops) are **blocked until
  `--allow-destructive`**: nothing executes at all while any is present.
- **Drift detection built for CI.** `check` plans once and exits `0` clean /
  `2` drift / `3` destructive drift, scriptable without parsing output.
  `--json` emits a stable versioned contract.
- **Safe under concurrency.** An advisory lock (keyed to the database, not
  the DSN) coordinates workers: one pusher at a time, losers wait bounded
  and re-verify, so deploy pipelines can race without corrupting anything.
- **Hypertables without hand-written SQL.** Decorate a model with
  `@hypertable` and the `create_hypertable` directive is planned
  state-aware: idempotent pushes, clean checks, no false drift.

PostgreSQL only, by design.

## Install

```console
pip install sqlpush
```

Or from source:

```console
git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
```

## The 30-second tour

Point sqlpush at your metadata (`module:attribute`) and a database
(`--dsn` or `$DATABASE_URL`):

```console
$ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"

$ sqlpush diff "myapp.models:metadata"
-- safe

CREATE TABLE hero (
    id SERIAL NOT NULL PRIMARY KEY,
    name VARCHAR(50) NOT NULL
);

-- risky

CREATE INDEX ix_hero_name ON hero (name);
```

Push it (the destructive gate is on by default):

```console
$ sqlpush push "myapp.models:metadata"
1 destructive operation(s) blocked; re-run with --allow-destructive
$ echo $?
1

$ sqlpush push "myapp.models:metadata" --allow-destructive
$ echo $?
0
```

In CI, check drift and fail loudly (see exit codes below). Limit scope with
repeated `--schema` / `--exclude` options.

## Exit codes

| verb | 0 | 1 | 2 | 3 |
| --- | --- | --- | --- | --- |
| `diff` | always | | | |
| `check` | clean | | drift | destructive drift |
| `push` | applied | destructive blocked | error (incl. partial failure) | |

`push --safe-only` runs only safe operations and skips the rest
informationally (exit `0`). A failed `CREATE INDEX CONCURRENTLY` marks the
run as partial failure (exit `2`) instead of silently half-applying.

## FastAPI / SQLModel: replace `create_all`

```python
from contextlib import asynccontextmanager
from sqlpush import aensure_schema


@asynccontextmanager
async def lifespan(app):
    await aensure_schema(SQLModel.metadata, engine, mode="check")
    yield
```

Push in the deploy pipeline, check at startup.

## How it works

```mermaid
flowchart LR
    models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
    db[("live PostgreSQL")] --> diff
    diff --> risk["risk classification<br>safe / risky / destructive"]
    risk --> plan["plan"]
    plan --> render["render"]
    render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
    apply --> report["report"]
```

- **Diff engine** scopes reflection to your target schemas (default: the
  session's real `search_path`) and prunes system catalogs (TimescaleDB
  internals included) before reflection even starts.
- **Classifier** maps each operation to a risk class; unknown operations
  are `risky`, never silently safe.
- **Executor** splits the plan: `CONCURRENTLY` statements run one-per-
  transaction on autocommit, everything else applies in a single atomic
  transaction with a bounded `lock_timeout`.
- **Typed errors**: only `SqlpushError` / `ConnectFailed` /
  `MetadataImportError` escape the API, never raw driver exceptions.

## Comparison

An honest view of the neighborhood (stars as of 2026-08):

| | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
| --- | --- | --- | --- | --- | --- |
| **sqlpush** | none (the diff is the migration) | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
| [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
| [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
| [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
| [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |

sqlpush is narrower than atlas and younger than alembic, deliberately.
It is one tool for one job: keep a PostgreSQL schema in lockstep with
SQLAlchemy models, safely enough to run from CI.

Coming from [migra](https://github.com/djrobstep/migra) (now
deprecated)? There is a [migration guide](docs/migrating-from-migra.md).

## Design notes

- `import sqlpush` stays light: the public API loads lazily, so the
  annotations module carries none of alembic/typer/psycopg.
- The advisory-lock key derives from the database OID: two DSN spellings
  of the same database contend for the same lock.
- `--json` output is a versioned contract (`"version": 1`) meant for
  tooling; additive changes only within a version.

## Roadmap (0.1.x)

- `CREATE INDEX CONCURRENTLY` by default for indexes on existing tables
- asyncpg DSN translation in `ensure_schema(AsyncEngine)`
- jsonschema-validated `--json` output

## License

[MIT](LICENSE) · © 2026 Juan Miguel Contreras
