sqlpush
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.
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.
checkplans once and exits0clean /2drift /3destructive drift, scriptable without parsing output.--jsonemits 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
@hypertableand thecreate_hypertabledirective is planned state-aware: idempotent pushes, clean checks, no false drift.
PostgreSQL only, by design.
Install
pip install sqlpush
Or from source:
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):
$ 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):
$ 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
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
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:
CONCURRENTLYstatements run one-per- transaction on autocommit, everything else applies in a single atomic transaction with a boundedlock_timeout. - Typed errors: only
SqlpushError/ConnectFailed/MetadataImportErrorescape 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 (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
| atlas (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
prisma db push (47k★) |
none | Prisma schema (Node/TS) | no | no | no |
| 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 (now deprecated)? There is a migration guide.
Design notes
import sqlpushstays 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.
--jsonoutput is a versioned contract ("version": 1) meant for tooling; additive changes only within a version.
Roadmap (0.1.x)
CREATE INDEX CONCURRENTLYby default for indexes on existing tables- asyncpg DSN translation in
ensure_schema(AsyncEngine) - jsonschema-validated
--jsonoutput
License
MIT · © 2026 Juan Miguel Contreras
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlpush-0.4.0.tar.gz.
File metadata
- Download URL: sqlpush-0.4.0.tar.gz
- Upload date:
- Size: 25.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51469fe3f41d51e9eccdfcce0d7e7321aa224b0f0e5b289f64f03602f2279cd0
|
|
| MD5 |
819b72dd9c244e248dd2bc3bd646f71e
|
|
| BLAKE2b-256 |
60bd8d75b068adee5cd681ae9f25d47eab90eb79150547f5281279ec30eaaf49
|
Provenance
The following attestation bundles were made for sqlpush-0.4.0.tar.gz:
Publisher:
release.yml on juanmicl/sqlpush
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlpush-0.4.0.tar.gz -
Subject digest:
51469fe3f41d51e9eccdfcce0d7e7321aa224b0f0e5b289f64f03602f2279cd0 - Sigstore transparency entry: 2681878157
- Sigstore integration time:
-
Permalink:
juanmicl/sqlpush@30869d80a192f0923d83c277e720833f79cad034 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/juanmicl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@30869d80a192f0923d83c277e720833f79cad034 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sqlpush-0.4.0-py3-none-any.whl.
File metadata
- Download URL: sqlpush-0.4.0-py3-none-any.whl
- Upload date:
- Size: 32.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bee6832a03125abd0506d09ff4feb4d5e579283b3ff57eb590e6203605532986
|
|
| MD5 |
58476e8a48aab1bc01d2b82cd7e39a40
|
|
| BLAKE2b-256 |
3c973e5f9751b1c2cc90d1097559654c121d253277d897c579877e0ca7b29da3
|
Provenance
The following attestation bundles were made for sqlpush-0.4.0-py3-none-any.whl:
Publisher:
release.yml on juanmicl/sqlpush
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlpush-0.4.0-py3-none-any.whl -
Subject digest:
bee6832a03125abd0506d09ff4feb4d5e579283b3ff57eb590e6203605532986 - Sigstore transparency entry: 2681878179
- Sigstore integration time:
-
Permalink:
juanmicl/sqlpush@30869d80a192f0923d83c277e720833f79cad034 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/juanmicl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@30869d80a192f0923d83c277e720833f79cad034 -
Trigger Event:
push
-
Statement type: