sqlpush 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlpush-0.5.1/PKG-INFO ADDED
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlpush
3
+ Version: 0.5.1
4
+ Summary: Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models
5
+ Keywords: sqlalchemy,sqlmodel,fastapi,alembic,postgresql,timescaledb,prisma,schema,migrations,database,drift,cli
6
+ Author: Juan Miguel Contreras
7
+ Author-email: Juan Miguel Contreras <19253629+juanmicl@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Dist: alembic>=1.18,<2
20
+ Requires-Dist: psycopg[binary]>=3.2
21
+ Requires-Dist: sqlalchemy>=2.0
22
+ Requires-Dist: typer>=0.12
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/juanmicl/sqlpush
25
+ Project-URL: Repository, https://github.com/juanmicl/sqlpush
26
+ Project-URL: Issues, https://github.com/juanmicl/sqlpush/issues
27
+ Project-URL: Changelog, https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md
28
+ Description-Content-Type: text/markdown
29
+
30
+ # sqlpush
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
35
+
36
+ **Prisma `db push` for SQLAlchemy.** Your models are the migration.
37
+ sqlpush diffs them (SQLAlchemy, SQLModel, anything built on `MetaData`)
38
+ against the live PostgreSQL / TimescaleDB database, classifies every
39
+ operation by risk (safe / risky / destructive), and applies the plan
40
+ atomically. No migration files to write, no `upgrade` step to forget.
41
+ Drift checks exit with codes your CI can gate on.
42
+
43
+ ```console
44
+ sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
45
+ sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
46
+ sqlpush push "myapp.models:metadata" # apply (destructive gated)
47
+ ```
48
+
49
+ If you've run `Base.metadata.create_all()` in production and known it
50
+ was wrong, sqlpush is for you.
51
+
52
+ ## Install
53
+
54
+ ```console
55
+ pip install sqlpush
56
+ ```
57
+
58
+ Or from source:
59
+
60
+ ```console
61
+ git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
62
+ ```
63
+
64
+ Python 3.10 or newer. PostgreSQL only.
65
+
66
+ ## Why
67
+
68
+ Declarative models are already the source of truth. Migration files
69
+ re-encode what the models say, drift from them, and pile up forever.
70
+ sqlpush closes the loop the way Prisma's `db push` does for its schema
71
+ language, but for the SQLAlchemy ecosystem (SQLModel included):
72
+
73
+ - **No migration files required.** The diff *is* the migration: computed fresh
74
+ from models vs. live database on every run, via alembic's autogenerate
75
+ engine used as a library. Files exist as a second workflow when you want
76
+ them (see below).
77
+ - **Risk-aware by default.** Every operation is classified `safe` /
78
+ `risky` / `destructive`. Destructive ops (drops) are **blocked until
79
+ `--allow-destructive`**: nothing executes at all while any is present.
80
+ - **Drift detection built for CI.** `check` plans once and reports
81
+ through its exit code, no output parsing; `--json` emits a stable
82
+ versioned contract.
83
+ - **Safe under concurrency.** An advisory lock coordinates workers: one
84
+ pusher at a time, losers wait bounded
85
+ and re-verify, so deploy pipelines can race without corrupting anything.
86
+ - **Hypertables without hand-written SQL.** Decorate a model with
87
+ `@hypertable` and the `create_hypertable` directive is planned
88
+ state-aware: idempotent pushes, clean checks, no false drift.
89
+
90
+ If you know alembic: sqlpush is its autogenerate engine, productized
91
+ into apply and check verbs, with no revision scripts to maintain.
92
+
93
+ PostgreSQL only, by design.
94
+
95
+ ## The 30-second tour
96
+
97
+ Point sqlpush at your metadata (`module:attribute`) and a database
98
+ (`--dsn` or `$DATABASE_URL`):
99
+
100
+ ```console
101
+ $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
102
+
103
+ $ sqlpush diff "myapp.models:metadata"
104
+ -- safe
105
+ CREATE TABLE hero (
106
+ id SERIAL NOT NULL,
107
+ name VARCHAR(50) NOT NULL,
108
+ PRIMARY KEY (id)
109
+ );
110
+
111
+ -- risky
112
+ CREATE INDEX ix_hero_name ON hero (name);
113
+ ```
114
+
115
+ Push it (the destructive gate is on by default):
116
+
117
+ ```console
118
+ $ sqlpush push "myapp.models:metadata"
119
+ 1 destructive operation(s) blocked; re-run with --allow-destructive
120
+ $ echo $?
121
+ 1
122
+
123
+ $ sqlpush push "myapp.models:metadata" --allow-destructive
124
+ $ echo $?
125
+ 0
126
+ ```
127
+
128
+ In CI, check drift and fail loudly (see exit codes below). Limit scope with
129
+ repeated `--schema` / `--exclude` options.
130
+
131
+ ## FastAPI: retire `create_all()`
132
+
133
+ Most FastAPI + SQLModel apps ship the lifespan the tutorials teach:
134
+
135
+ ```python
136
+ @asynccontextmanager
137
+ async def lifespan(app):
138
+ async with engine.begin() as conn:
139
+ await conn.run_sync(SQLModel.metadata.create_all)
140
+ yield
141
+ ```
142
+
143
+ `create_all` creates tables that are missing. That is all it ever
144
+ does. Add a column to a model and the database never hears about it;
145
+ an index on an existing table, a type change, a drop: nothing.
146
+ Production drifts from the models in silence, so every real change
147
+ still rides the alembic treadmill: autogenerate, review, upgrade, and
148
+ two histories to keep in agreement forever.
149
+
150
+ The sqlpush lifespan is one line:
151
+
152
+ ```python
153
+ from contextlib import asynccontextmanager
154
+ from sqlpush import aensure_schema
155
+
156
+
157
+ @asynccontextmanager
158
+ async def lifespan(app):
159
+ await aensure_schema(SQLModel.metadata, engine, mode="check")
160
+ yield
161
+ ```
162
+
163
+ `mode="check"` verifies the models against the database at startup and
164
+ raises when they disagree: the app refuses to boot against a schema it
165
+ does not match, which beats failing on the first query at 3am. The
166
+ schema change itself comes from wherever you put it: `sqlpush push` in
167
+ the deploy pipeline (destructive ops gated), or
168
+ `aensure_schema(..., mode="push")` when you want the API to apply it.
169
+
170
+ asyncpg URLs work too: a DSN or `AsyncEngine` spelling
171
+ `postgresql+asyncpg` is translated to the psycopg driver automatically,
172
+ and asyncpg is never required in the sqlpush process.
173
+
174
+ Push in the deploy pipeline, check at boot.
175
+
176
+ ## An inherited database
177
+
178
+ The first `check` against a database with history often reports drift:
179
+ hand-built indexes, audit tables, a column someone added by hand. If
180
+ any of the drift looks destructive, `check` exits `3` and `push`
181
+ blocks. That is the tool refusing to silently drop your legacy
182
+ objects. Two escape hatches: `--exclude` accepts objects you choose to
183
+ keep (fnmatch patterns, repeatable), and `--allow-destructive` accepts
184
+ the drops when you really do want them.
185
+
186
+ ## When you want files: the chain
187
+
188
+ Most changes never need a file. When one does, sqlpush has a second
189
+ workflow built on the same diff engine: the chain. `revision` writes
190
+ the next numbered SQL file from your models against a reference DB,
191
+ `migrate` replays pending files with gates and checksums, and `stamp`
192
+ adopts an existing database without executing anything.
193
+
194
+ The files are plain SQL you can review, edit before first apply, and
195
+ run under `psql`. Schema change and data backfill ship as one file.
196
+ The [chain guide](docs/the-chain.md) covers the format, the gates and
197
+ the workflows.
198
+
199
+ ## Exit codes
200
+
201
+ | verb | 0 | 1 | 2 | 3 |
202
+ | --- | --- | --- | --- | --- |
203
+ | `diff` | always | | | |
204
+ | `check` | clean | | drift | destructive drift |
205
+ | `push` | applied | destructive blocked | error (incl. partial failure) | |
206
+ | `revision` | file written | error (empty drift refuses) | | |
207
+ | `migrate` | clean | blocked or partial failure | | |
208
+ | `stamp` | registered | blocked or refused | | |
209
+
210
+ Failures print a typed error on stderr, never a traceback.
211
+
212
+ `push --safe-only` runs only safe operations and skips the rest
213
+ informationally (exit `0`). Indexes on existing tables build
214
+ `CONCURRENTLY` by default (opt out with `--no-concurrently`); a failed
215
+ `CREATE INDEX CONCURRENTLY` marks the run as partial failure (exit `2`)
216
+ instead of silently half-applying, and leaves an INVALID index: drop it
217
+ (`DROP INDEX CONCURRENTLY`) and re-push. `stamp` refuses a file whose
218
+ checksum no longer matches the registry; `--force` accepts the new
219
+ content.
220
+
221
+ The knobs, per verb:
222
+
223
+ | verb | flags |
224
+ | --- | --- |
225
+ | `push` | `--allow-destructive` `--safe-only` `--no-lock` `--lock-timeout` `--advisory-wait` `--no-concurrently` `--statement-timeout` |
226
+ | `revision` | `--ref-dsn` (required) `-m/--message` `--no-concurrently` `--dir` |
227
+ | `migrate` | `--allow-destructive` `--advisory-wait` `--lock-timeout` `--statement-timeout` `--dir` |
228
+ | `stamp` | `--force` `--dir` |
229
+
230
+ Every verb except `revision` takes `--dsn` (or `$DATABASE_URL`).
231
+ `revision` requires `--ref-dsn`, with no env fallback: the reference
232
+ DB is a different database from the push target. `diff`, `check`,
233
+ `push` and `revision` also take repeatable `--schema` / `--exclude`.
234
+ Timeouts are seconds; a `lock_timeout` bounds how long a statement
235
+ waits on a lock before failing, `statement_timeout` bounds each
236
+ statement's runtime, and an exhausted `advisory-wait` raises instead
237
+ of hanging on a stuck lock holder.
238
+
239
+ ## How it works
240
+
241
+ ```mermaid
242
+ flowchart LR
243
+ models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
244
+ db[("live PostgreSQL")] --> diff
245
+ diff --> risk["risk classification<br>safe / risky / destructive"]
246
+ risk --> plan["plan"]
247
+ plan --> render["render"]
248
+ render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
249
+ apply --> report["report"]
250
+ ```
251
+
252
+ - **Diff engine** scopes reflection to your target schemas and prunes
253
+ system catalogs (TimescaleDB internals included) before reflection
254
+ even starts.
255
+ - **Classifier** maps each operation to a risk class; unknown operations
256
+ are `risky`, never silently safe.
257
+ - **Executor** splits the plan: concurrent index builds run one per
258
+ transaction on autocommit, everything else applies in a single atomic
259
+ transaction with a bounded `lock_timeout`.
260
+ - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
261
+ `MetadataImportError` escape the API, never raw driver exceptions.
262
+
263
+ Scoping: `--schema` restricts the diff to named schemas (default: the
264
+ session's real `search_path`). Extension-owned schemas never enter
265
+ scope automatically, and schemas you pass explicitly are never
266
+ filtered. The chain's registry table (`public.sqlpush_versions`)
267
+ always lives in `public` and is pruned from every diff, so `check`
268
+ after `migrate` is clean. `alembic_version` gets the same treatment.
269
+
270
+ ## Comparison
271
+
272
+ An honest view of the neighborhood (stars as of 2026-08):
273
+
274
+ | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
275
+ | --- | --- | --- | --- | --- | --- |
276
+ | **sqlpush** | optional: push needs none; the chain has reviewable, checksummed files | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
277
+ | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
278
+ | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
279
+ | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
280
+ | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
281
+
282
+ sqlpush is narrower than atlas and younger than alembic, deliberately.
283
+ It is one tool for one job: keep a PostgreSQL schema in lockstep with
284
+ SQLAlchemy models, safely enough to run from CI.
285
+
286
+ Guides: [the chain](docs/the-chain.md) (file format, gates, backfills),
287
+ [migrating from alembic](docs/migrating-from-alembic.md), and
288
+ [migrating from migra](docs/migrating-from-migra.md) (deprecated).
289
+ Changes land in the [CHANGELOG](CHANGELOG.md).
290
+
291
+ ## Design notes
292
+
293
+ - `import sqlpush` stays light: the public API loads lazily, so the
294
+ annotations module carries none of alembic/typer/psycopg.
295
+ - The advisory-lock key derives from the database OID: two DSN spellings
296
+ of the same database contend for the same lock.
297
+ - `--json` output is a versioned contract (`"version": 1`) meant for
298
+ tooling; additive changes only within a version (operations now carry
299
+ a `concurrent` boolean).
@@ -0,0 +1,270 @@
1
+ # sqlpush
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
6
+
7
+ **Prisma `db push` for SQLAlchemy.** Your models are the migration.
8
+ sqlpush diffs them (SQLAlchemy, SQLModel, anything built on `MetaData`)
9
+ against the live PostgreSQL / TimescaleDB database, classifies every
10
+ operation by risk (safe / risky / destructive), and applies the plan
11
+ atomically. No migration files to write, no `upgrade` step to forget.
12
+ Drift checks exit with codes your CI can gate on.
13
+
14
+ ```console
15
+ sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
16
+ sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
17
+ sqlpush push "myapp.models:metadata" # apply (destructive gated)
18
+ ```
19
+
20
+ If you've run `Base.metadata.create_all()` in production and known it
21
+ was wrong, sqlpush is for you.
22
+
23
+ ## Install
24
+
25
+ ```console
26
+ pip install sqlpush
27
+ ```
28
+
29
+ Or from source:
30
+
31
+ ```console
32
+ git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
33
+ ```
34
+
35
+ Python 3.10 or newer. PostgreSQL only.
36
+
37
+ ## Why
38
+
39
+ Declarative models are already the source of truth. Migration files
40
+ re-encode what the models say, drift from them, and pile up forever.
41
+ sqlpush closes the loop the way Prisma's `db push` does for its schema
42
+ language, but for the SQLAlchemy ecosystem (SQLModel included):
43
+
44
+ - **No migration files required.** The diff *is* the migration: computed fresh
45
+ from models vs. live database on every run, via alembic's autogenerate
46
+ engine used as a library. Files exist as a second workflow when you want
47
+ them (see below).
48
+ - **Risk-aware by default.** Every operation is classified `safe` /
49
+ `risky` / `destructive`. Destructive ops (drops) are **blocked until
50
+ `--allow-destructive`**: nothing executes at all while any is present.
51
+ - **Drift detection built for CI.** `check` plans once and reports
52
+ through its exit code, no output parsing; `--json` emits a stable
53
+ versioned contract.
54
+ - **Safe under concurrency.** An advisory lock coordinates workers: one
55
+ pusher at a time, losers wait bounded
56
+ and re-verify, so deploy pipelines can race without corrupting anything.
57
+ - **Hypertables without hand-written SQL.** Decorate a model with
58
+ `@hypertable` and the `create_hypertable` directive is planned
59
+ state-aware: idempotent pushes, clean checks, no false drift.
60
+
61
+ If you know alembic: sqlpush is its autogenerate engine, productized
62
+ into apply and check verbs, with no revision scripts to maintain.
63
+
64
+ PostgreSQL only, by design.
65
+
66
+ ## The 30-second tour
67
+
68
+ Point sqlpush at your metadata (`module:attribute`) and a database
69
+ (`--dsn` or `$DATABASE_URL`):
70
+
71
+ ```console
72
+ $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
73
+
74
+ $ sqlpush diff "myapp.models:metadata"
75
+ -- safe
76
+ CREATE TABLE hero (
77
+ id SERIAL NOT NULL,
78
+ name VARCHAR(50) NOT NULL,
79
+ PRIMARY KEY (id)
80
+ );
81
+
82
+ -- risky
83
+ CREATE INDEX ix_hero_name ON hero (name);
84
+ ```
85
+
86
+ Push it (the destructive gate is on by default):
87
+
88
+ ```console
89
+ $ sqlpush push "myapp.models:metadata"
90
+ 1 destructive operation(s) blocked; re-run with --allow-destructive
91
+ $ echo $?
92
+ 1
93
+
94
+ $ sqlpush push "myapp.models:metadata" --allow-destructive
95
+ $ echo $?
96
+ 0
97
+ ```
98
+
99
+ In CI, check drift and fail loudly (see exit codes below). Limit scope with
100
+ repeated `--schema` / `--exclude` options.
101
+
102
+ ## FastAPI: retire `create_all()`
103
+
104
+ Most FastAPI + SQLModel apps ship the lifespan the tutorials teach:
105
+
106
+ ```python
107
+ @asynccontextmanager
108
+ async def lifespan(app):
109
+ async with engine.begin() as conn:
110
+ await conn.run_sync(SQLModel.metadata.create_all)
111
+ yield
112
+ ```
113
+
114
+ `create_all` creates tables that are missing. That is all it ever
115
+ does. Add a column to a model and the database never hears about it;
116
+ an index on an existing table, a type change, a drop: nothing.
117
+ Production drifts from the models in silence, so every real change
118
+ still rides the alembic treadmill: autogenerate, review, upgrade, and
119
+ two histories to keep in agreement forever.
120
+
121
+ The sqlpush lifespan is one line:
122
+
123
+ ```python
124
+ from contextlib import asynccontextmanager
125
+ from sqlpush import aensure_schema
126
+
127
+
128
+ @asynccontextmanager
129
+ async def lifespan(app):
130
+ await aensure_schema(SQLModel.metadata, engine, mode="check")
131
+ yield
132
+ ```
133
+
134
+ `mode="check"` verifies the models against the database at startup and
135
+ raises when they disagree: the app refuses to boot against a schema it
136
+ does not match, which beats failing on the first query at 3am. The
137
+ schema change itself comes from wherever you put it: `sqlpush push` in
138
+ the deploy pipeline (destructive ops gated), or
139
+ `aensure_schema(..., mode="push")` when you want the API to apply it.
140
+
141
+ asyncpg URLs work too: a DSN or `AsyncEngine` spelling
142
+ `postgresql+asyncpg` is translated to the psycopg driver automatically,
143
+ and asyncpg is never required in the sqlpush process.
144
+
145
+ Push in the deploy pipeline, check at boot.
146
+
147
+ ## An inherited database
148
+
149
+ The first `check` against a database with history often reports drift:
150
+ hand-built indexes, audit tables, a column someone added by hand. If
151
+ any of the drift looks destructive, `check` exits `3` and `push`
152
+ blocks. That is the tool refusing to silently drop your legacy
153
+ objects. Two escape hatches: `--exclude` accepts objects you choose to
154
+ keep (fnmatch patterns, repeatable), and `--allow-destructive` accepts
155
+ the drops when you really do want them.
156
+
157
+ ## When you want files: the chain
158
+
159
+ Most changes never need a file. When one does, sqlpush has a second
160
+ workflow built on the same diff engine: the chain. `revision` writes
161
+ the next numbered SQL file from your models against a reference DB,
162
+ `migrate` replays pending files with gates and checksums, and `stamp`
163
+ adopts an existing database without executing anything.
164
+
165
+ The files are plain SQL you can review, edit before first apply, and
166
+ run under `psql`. Schema change and data backfill ship as one file.
167
+ The [chain guide](docs/the-chain.md) covers the format, the gates and
168
+ the workflows.
169
+
170
+ ## Exit codes
171
+
172
+ | verb | 0 | 1 | 2 | 3 |
173
+ | --- | --- | --- | --- | --- |
174
+ | `diff` | always | | | |
175
+ | `check` | clean | | drift | destructive drift |
176
+ | `push` | applied | destructive blocked | error (incl. partial failure) | |
177
+ | `revision` | file written | error (empty drift refuses) | | |
178
+ | `migrate` | clean | blocked or partial failure | | |
179
+ | `stamp` | registered | blocked or refused | | |
180
+
181
+ Failures print a typed error on stderr, never a traceback.
182
+
183
+ `push --safe-only` runs only safe operations and skips the rest
184
+ informationally (exit `0`). Indexes on existing tables build
185
+ `CONCURRENTLY` by default (opt out with `--no-concurrently`); a failed
186
+ `CREATE INDEX CONCURRENTLY` marks the run as partial failure (exit `2`)
187
+ instead of silently half-applying, and leaves an INVALID index: drop it
188
+ (`DROP INDEX CONCURRENTLY`) and re-push. `stamp` refuses a file whose
189
+ checksum no longer matches the registry; `--force` accepts the new
190
+ content.
191
+
192
+ The knobs, per verb:
193
+
194
+ | verb | flags |
195
+ | --- | --- |
196
+ | `push` | `--allow-destructive` `--safe-only` `--no-lock` `--lock-timeout` `--advisory-wait` `--no-concurrently` `--statement-timeout` |
197
+ | `revision` | `--ref-dsn` (required) `-m/--message` `--no-concurrently` `--dir` |
198
+ | `migrate` | `--allow-destructive` `--advisory-wait` `--lock-timeout` `--statement-timeout` `--dir` |
199
+ | `stamp` | `--force` `--dir` |
200
+
201
+ Every verb except `revision` takes `--dsn` (or `$DATABASE_URL`).
202
+ `revision` requires `--ref-dsn`, with no env fallback: the reference
203
+ DB is a different database from the push target. `diff`, `check`,
204
+ `push` and `revision` also take repeatable `--schema` / `--exclude`.
205
+ Timeouts are seconds; a `lock_timeout` bounds how long a statement
206
+ waits on a lock before failing, `statement_timeout` bounds each
207
+ statement's runtime, and an exhausted `advisory-wait` raises instead
208
+ of hanging on a stuck lock holder.
209
+
210
+ ## How it works
211
+
212
+ ```mermaid
213
+ flowchart LR
214
+ models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
215
+ db[("live PostgreSQL")] --> diff
216
+ diff --> risk["risk classification<br>safe / risky / destructive"]
217
+ risk --> plan["plan"]
218
+ plan --> render["render"]
219
+ render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
220
+ apply --> report["report"]
221
+ ```
222
+
223
+ - **Diff engine** scopes reflection to your target schemas and prunes
224
+ system catalogs (TimescaleDB internals included) before reflection
225
+ even starts.
226
+ - **Classifier** maps each operation to a risk class; unknown operations
227
+ are `risky`, never silently safe.
228
+ - **Executor** splits the plan: concurrent index builds run one per
229
+ transaction on autocommit, everything else applies in a single atomic
230
+ transaction with a bounded `lock_timeout`.
231
+ - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
232
+ `MetadataImportError` escape the API, never raw driver exceptions.
233
+
234
+ Scoping: `--schema` restricts the diff to named schemas (default: the
235
+ session's real `search_path`). Extension-owned schemas never enter
236
+ scope automatically, and schemas you pass explicitly are never
237
+ filtered. The chain's registry table (`public.sqlpush_versions`)
238
+ always lives in `public` and is pruned from every diff, so `check`
239
+ after `migrate` is clean. `alembic_version` gets the same treatment.
240
+
241
+ ## Comparison
242
+
243
+ An honest view of the neighborhood (stars as of 2026-08):
244
+
245
+ | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
246
+ | --- | --- | --- | --- | --- | --- |
247
+ | **sqlpush** | optional: push needs none; the chain has reviewable, checksummed files | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
248
+ | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
249
+ | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
250
+ | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
251
+ | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
252
+
253
+ sqlpush is narrower than atlas and younger than alembic, deliberately.
254
+ It is one tool for one job: keep a PostgreSQL schema in lockstep with
255
+ SQLAlchemy models, safely enough to run from CI.
256
+
257
+ Guides: [the chain](docs/the-chain.md) (file format, gates, backfills),
258
+ [migrating from alembic](docs/migrating-from-alembic.md), and
259
+ [migrating from migra](docs/migrating-from-migra.md) (deprecated).
260
+ Changes land in the [CHANGELOG](CHANGELOG.md).
261
+
262
+ ## Design notes
263
+
264
+ - `import sqlpush` stays light: the public API loads lazily, so the
265
+ annotations module carries none of alembic/typer/psycopg.
266
+ - The advisory-lock key derives from the database OID: two DSN spellings
267
+ of the same database contend for the same lock.
268
+ - `--json` output is a versioned contract (`"version": 1`) meant for
269
+ tooling; additive changes only within a version (operations now carry
270
+ a `concurrent` boolean).
@@ -1,12 +1,14 @@
1
1
  [project]
2
2
  name = "sqlpush"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  license = "MIT"
8
8
  keywords = [
9
9
  "sqlalchemy",
10
+ "sqlmodel",
11
+ "fastapi",
10
12
  "alembic",
11
13
  "postgresql",
12
14
  "timescaledb",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlpush"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -10,6 +10,8 @@ requires-python = ">=3.10"
10
10
  license = "MIT"
11
11
  keywords = [
12
12
  "sqlalchemy",
13
+ "sqlmodel",
14
+ "fastapi",
13
15
  "alembic",
14
16
  "postgresql",
15
17
  "timescaledb",
sqlpush-0.5.0/PKG-INFO DELETED
@@ -1,218 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sqlpush
3
- Version: 0.5.0
4
- Summary: Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models
5
- Keywords: sqlalchemy,alembic,postgresql,timescaledb,prisma,schema,migrations,database,drift,cli
6
- Author: Juan Miguel Contreras
7
- Author-email: Juan Miguel Contreras <19253629+juanmicl@users.noreply.github.com>
8
- License-Expression: MIT
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Topic :: Database
18
- Classifier: Topic :: Software Development :: Quality Assurance
19
- Requires-Dist: alembic>=1.18,<2
20
- Requires-Dist: psycopg[binary]>=3.2
21
- Requires-Dist: sqlalchemy>=2.0
22
- Requires-Dist: typer>=0.12
23
- Requires-Python: >=3.10
24
- Project-URL: Homepage, https://github.com/juanmicl/sqlpush
25
- Project-URL: Repository, https://github.com/juanmicl/sqlpush
26
- Project-URL: Issues, https://github.com/juanmicl/sqlpush/issues
27
- Project-URL: Changelog, https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md
28
- Description-Content-Type: text/markdown
29
-
30
- # sqlpush
31
-
32
- [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
33
- [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
34
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
35
-
36
- **Prisma `db push` for SQLAlchemy.** Apply your models (SQLAlchemy,
37
- SQLModel, anything built on `MetaData`) to a live PostgreSQL / TimescaleDB
38
- database directly, no migration files. sqlpush
39
- diffs your models against the real schema, classifies every operation by
40
- risk (safe / risky / destructive), and applies the plan atomically. Drift
41
- checks exit with codes your CI can gate on.
42
-
43
- ```console
44
- sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
45
- sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
46
- sqlpush push "myapp.models:metadata" # apply (destructive gated)
47
- ```
48
-
49
- If you've ever run `Base.metadata.create_all()` in production and known it
50
- was wrong, then sighed at the migration-script treadmill when you reached
51
- for alembic: sqlpush is for you.
52
-
53
- ## Why
54
-
55
- Declarative models are already the source of truth. Migration files
56
- re-encode what the models say, drift from them, and pile up forever.
57
- sqlpush closes the loop the way Prisma's `db push` does for its schema
58
- language, but for the SQLAlchemy ecosystem (SQLModel included):
59
-
60
- - **No migration files, ever.** The diff *is* the migration: computed fresh
61
- from models vs. live database on every run, via alembic's autogenerate
62
- engine used as a library.
63
- - **Risk-aware by default.** Every operation is classified `safe` /
64
- `risky` / `destructive`. Destructive ops (drops) are **blocked until
65
- `--allow-destructive`**: nothing executes at all while any is present.
66
- - **Drift detection built for CI.** `check` plans once and exits `0` clean /
67
- `2` drift / `3` destructive drift, scriptable without parsing output.
68
- `--json` emits a stable versioned contract.
69
- - **Safe under concurrency.** An advisory lock (keyed to the database, not
70
- the DSN) coordinates workers: one pusher at a time, losers wait bounded
71
- and re-verify, so deploy pipelines can race without corrupting anything.
72
- - **Hypertables without hand-written SQL.** Decorate a model with
73
- `@hypertable` and the `create_hypertable` directive is planned
74
- state-aware: idempotent pushes, clean checks, no false drift.
75
-
76
- PostgreSQL only, by design.
77
-
78
- ## Install
79
-
80
- ```console
81
- pip install sqlpush
82
- ```
83
-
84
- Or from source:
85
-
86
- ```console
87
- git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
88
- ```
89
-
90
- ## The 30-second tour
91
-
92
- Point sqlpush at your metadata (`module:attribute`) and a database
93
- (`--dsn` or `$DATABASE_URL`):
94
-
95
- ```console
96
- $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
97
-
98
- $ sqlpush diff "myapp.models:metadata"
99
- -- safe
100
-
101
- CREATE TABLE hero (
102
- id SERIAL NOT NULL PRIMARY KEY,
103
- name VARCHAR(50) NOT NULL
104
- );
105
-
106
- -- risky
107
-
108
- CREATE INDEX ix_hero_name ON hero (name);
109
- ```
110
-
111
- Push it (the destructive gate is on by default):
112
-
113
- ```console
114
- $ sqlpush push "myapp.models:metadata"
115
- 1 destructive operation(s) blocked; re-run with --allow-destructive
116
- $ echo $?
117
- 1
118
-
119
- $ sqlpush push "myapp.models:metadata" --allow-destructive
120
- $ echo $?
121
- 0
122
- ```
123
-
124
- In CI, check drift and fail loudly (see exit codes below). Limit scope with
125
- repeated `--schema` / `--exclude` options.
126
-
127
- ## Exit codes
128
-
129
- | verb | 0 | 1 | 2 | 3 |
130
- | --- | --- | --- | --- | --- |
131
- | `diff` | always | | | |
132
- | `check` | clean | | drift | destructive drift |
133
- | `push` | applied | destructive blocked | error (incl. partial failure) | |
134
-
135
- `push --safe-only` runs only safe operations and skips the rest
136
- informationally (exit `0`). Indexes on existing tables build
137
- `CONCURRENTLY` by default (opt out with `--no-concurrently`); a failed
138
- `CREATE INDEX CONCURRENTLY` marks the run as partial failure (exit `2`)
139
- instead of silently half-applying, and leaves an INVALID index — drop it
140
- (`DROP INDEX CONCURRENTLY`) and re-push.
141
-
142
- ## FastAPI / SQLModel: replace `create_all`
143
-
144
- ```python
145
- from contextlib import asynccontextmanager
146
- from sqlpush import aensure_schema
147
-
148
-
149
- @asynccontextmanager
150
- async def lifespan(app):
151
- await aensure_schema(SQLModel.metadata, engine, mode="check")
152
- yield
153
- ```
154
-
155
- Push in the deploy pipeline, check at startup.
156
-
157
- ## How it works
158
-
159
- ```mermaid
160
- flowchart LR
161
- models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
162
- db[("live PostgreSQL")] --> diff
163
- diff --> risk["risk classification<br>safe / risky / destructive"]
164
- risk --> plan["plan"]
165
- plan --> render["render"]
166
- render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
167
- apply --> report["report"]
168
- ```
169
-
170
- - **Diff engine** scopes reflection to your target schemas (default: the
171
- session's real `search_path`) and prunes system catalogs (TimescaleDB
172
- internals included) before reflection even starts.
173
- - **Classifier** maps each operation to a risk class; unknown operations
174
- are `risky`, never silently safe.
175
- - **Executor** splits the plan: existing-table indexes render
176
- `CREATE INDEX CONCURRENTLY` and run one-per-transaction on autocommit
177
- (`--no-concurrently` opts out; indexes on tables the same plan creates
178
- stay in the atomic transaction), everything else applies in a single
179
- atomic transaction with a bounded `lock_timeout`.
180
- - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
181
- `MetadataImportError` escape the API, never raw driver exceptions.
182
-
183
- ## Comparison
184
-
185
- An honest view of the neighborhood (stars as of 2026-08):
186
-
187
- | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
188
- | --- | --- | --- | --- | --- | --- |
189
- | **sqlpush** | none (the diff is the migration) | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
190
- | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
191
- | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
192
- | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
193
- | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
194
-
195
- sqlpush is narrower than atlas and younger than alembic, deliberately.
196
- It is one tool for one job: keep a PostgreSQL schema in lockstep with
197
- SQLAlchemy models, safely enough to run from CI.
198
-
199
- Coming from [migra](https://github.com/djrobstep/migra) (now
200
- deprecated)? There is a [migration guide](docs/migrating-from-migra.md).
201
-
202
- ## Design notes
203
-
204
- - `import sqlpush` stays light: the public API loads lazily, so the
205
- annotations module carries none of alembic/typer/psycopg.
206
- - The advisory-lock key derives from the database OID: two DSN spellings
207
- of the same database contend for the same lock.
208
- - `--json` output is a versioned contract (`"version": 1`) meant for
209
- tooling; additive changes only within a version (operations now carry
210
- a `concurrent` boolean).
211
-
212
- ## Roadmap (0.1.x)
213
-
214
- - jsonschema-validated `--json` output
215
-
216
- ## License
217
-
218
- [MIT](LICENSE) · © 2026 Juan Miguel Contreras
sqlpush-0.5.0/README.md DELETED
@@ -1,189 +0,0 @@
1
- # sqlpush
2
-
3
- [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
4
- [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
6
-
7
- **Prisma `db push` for SQLAlchemy.** Apply your models (SQLAlchemy,
8
- SQLModel, anything built on `MetaData`) to a live PostgreSQL / TimescaleDB
9
- database directly, no migration files. sqlpush
10
- diffs your models against the real schema, classifies every operation by
11
- risk (safe / risky / destructive), and applies the plan atomically. Drift
12
- checks exit with codes your CI can gate on.
13
-
14
- ```console
15
- sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
16
- sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
17
- sqlpush push "myapp.models:metadata" # apply (destructive gated)
18
- ```
19
-
20
- If you've ever run `Base.metadata.create_all()` in production and known it
21
- was wrong, then sighed at the migration-script treadmill when you reached
22
- for alembic: sqlpush is for you.
23
-
24
- ## Why
25
-
26
- Declarative models are already the source of truth. Migration files
27
- re-encode what the models say, drift from them, and pile up forever.
28
- sqlpush closes the loop the way Prisma's `db push` does for its schema
29
- language, but for the SQLAlchemy ecosystem (SQLModel included):
30
-
31
- - **No migration files, ever.** The diff *is* the migration: computed fresh
32
- from models vs. live database on every run, via alembic's autogenerate
33
- engine used as a library.
34
- - **Risk-aware by default.** Every operation is classified `safe` /
35
- `risky` / `destructive`. Destructive ops (drops) are **blocked until
36
- `--allow-destructive`**: nothing executes at all while any is present.
37
- - **Drift detection built for CI.** `check` plans once and exits `0` clean /
38
- `2` drift / `3` destructive drift, scriptable without parsing output.
39
- `--json` emits a stable versioned contract.
40
- - **Safe under concurrency.** An advisory lock (keyed to the database, not
41
- the DSN) coordinates workers: one pusher at a time, losers wait bounded
42
- and re-verify, so deploy pipelines can race without corrupting anything.
43
- - **Hypertables without hand-written SQL.** Decorate a model with
44
- `@hypertable` and the `create_hypertable` directive is planned
45
- state-aware: idempotent pushes, clean checks, no false drift.
46
-
47
- PostgreSQL only, by design.
48
-
49
- ## Install
50
-
51
- ```console
52
- pip install sqlpush
53
- ```
54
-
55
- Or from source:
56
-
57
- ```console
58
- git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
59
- ```
60
-
61
- ## The 30-second tour
62
-
63
- Point sqlpush at your metadata (`module:attribute`) and a database
64
- (`--dsn` or `$DATABASE_URL`):
65
-
66
- ```console
67
- $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
68
-
69
- $ sqlpush diff "myapp.models:metadata"
70
- -- safe
71
-
72
- CREATE TABLE hero (
73
- id SERIAL NOT NULL PRIMARY KEY,
74
- name VARCHAR(50) NOT NULL
75
- );
76
-
77
- -- risky
78
-
79
- CREATE INDEX ix_hero_name ON hero (name);
80
- ```
81
-
82
- Push it (the destructive gate is on by default):
83
-
84
- ```console
85
- $ sqlpush push "myapp.models:metadata"
86
- 1 destructive operation(s) blocked; re-run with --allow-destructive
87
- $ echo $?
88
- 1
89
-
90
- $ sqlpush push "myapp.models:metadata" --allow-destructive
91
- $ echo $?
92
- 0
93
- ```
94
-
95
- In CI, check drift and fail loudly (see exit codes below). Limit scope with
96
- repeated `--schema` / `--exclude` options.
97
-
98
- ## Exit codes
99
-
100
- | verb | 0 | 1 | 2 | 3 |
101
- | --- | --- | --- | --- | --- |
102
- | `diff` | always | | | |
103
- | `check` | clean | | drift | destructive drift |
104
- | `push` | applied | destructive blocked | error (incl. partial failure) | |
105
-
106
- `push --safe-only` runs only safe operations and skips the rest
107
- informationally (exit `0`). Indexes on existing tables build
108
- `CONCURRENTLY` by default (opt out with `--no-concurrently`); a failed
109
- `CREATE INDEX CONCURRENTLY` marks the run as partial failure (exit `2`)
110
- instead of silently half-applying, and leaves an INVALID index — drop it
111
- (`DROP INDEX CONCURRENTLY`) and re-push.
112
-
113
- ## FastAPI / SQLModel: replace `create_all`
114
-
115
- ```python
116
- from contextlib import asynccontextmanager
117
- from sqlpush import aensure_schema
118
-
119
-
120
- @asynccontextmanager
121
- async def lifespan(app):
122
- await aensure_schema(SQLModel.metadata, engine, mode="check")
123
- yield
124
- ```
125
-
126
- Push in the deploy pipeline, check at startup.
127
-
128
- ## How it works
129
-
130
- ```mermaid
131
- flowchart LR
132
- models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
133
- db[("live PostgreSQL")] --> diff
134
- diff --> risk["risk classification<br>safe / risky / destructive"]
135
- risk --> plan["plan"]
136
- plan --> render["render"]
137
- render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
138
- apply --> report["report"]
139
- ```
140
-
141
- - **Diff engine** scopes reflection to your target schemas (default: the
142
- session's real `search_path`) and prunes system catalogs (TimescaleDB
143
- internals included) before reflection even starts.
144
- - **Classifier** maps each operation to a risk class; unknown operations
145
- are `risky`, never silently safe.
146
- - **Executor** splits the plan: existing-table indexes render
147
- `CREATE INDEX CONCURRENTLY` and run one-per-transaction on autocommit
148
- (`--no-concurrently` opts out; indexes on tables the same plan creates
149
- stay in the atomic transaction), everything else applies in a single
150
- atomic transaction with a bounded `lock_timeout`.
151
- - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
152
- `MetadataImportError` escape the API, never raw driver exceptions.
153
-
154
- ## Comparison
155
-
156
- An honest view of the neighborhood (stars as of 2026-08):
157
-
158
- | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
159
- | --- | --- | --- | --- | --- | --- |
160
- | **sqlpush** | none (the diff is the migration) | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
161
- | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
162
- | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
163
- | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
164
- | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
165
-
166
- sqlpush is narrower than atlas and younger than alembic, deliberately.
167
- It is one tool for one job: keep a PostgreSQL schema in lockstep with
168
- SQLAlchemy models, safely enough to run from CI.
169
-
170
- Coming from [migra](https://github.com/djrobstep/migra) (now
171
- deprecated)? There is a [migration guide](docs/migrating-from-migra.md).
172
-
173
- ## Design notes
174
-
175
- - `import sqlpush` stays light: the public API loads lazily, so the
176
- annotations module carries none of alembic/typer/psycopg.
177
- - The advisory-lock key derives from the database OID: two DSN spellings
178
- of the same database contend for the same lock.
179
- - `--json` output is a versioned contract (`"version": 1`) meant for
180
- tooling; additive changes only within a version (operations now carry
181
- a `concurrent` boolean).
182
-
183
- ## Roadmap (0.1.x)
184
-
185
- - jsonschema-validated `--json` output
186
-
187
- ## License
188
-
189
- [MIT](LICENSE) · © 2026 Juan Miguel Contreras
File without changes
File without changes
File without changes
File without changes