xtr-orm 1.5.0__tar.gz → 3.0.0__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.
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/PKG-INFO +22 -13
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/README.md +16 -7
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/pyproject.toml +14 -11
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/pyproject.toml.orig +15 -11
- xtr_orm-3.0.0/src/xtr_orm/.agents/skills/xtr-orm/SKILL.md +282 -0
- xtr_orm-3.0.0/src/xtr_orm/.agents/skills/xtr-orm/references/commands.md +67 -0
- xtr_orm-3.0.0/src/xtr_orm/.agents/skills/xtr-orm/references/connections.md +97 -0
- xtr_orm-3.0.0/src/xtr_orm/.agents/skills/xtr-orm/references/messenger.md +95 -0
- xtr_orm-3.0.0/src/xtr_orm/.agents/skills/xtr-orm/references/migrations.md +71 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/bundle/connection_config.py +3 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/bundle/orm_bundle.py +101 -64
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/connection_command.py +18 -3
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/database_create_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/database_drop_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_current_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_diff_command.py +3 -3
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_dump_schema_command.py +2 -2
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_execute_command.py +3 -3
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_generate_command.py +2 -2
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_latest_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_list_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_migrate_command.py +6 -8
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_rollup_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_status_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_up_to_date_command.py +3 -8
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/migrations_version_command.py +4 -3
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/run_sql_command.py +1 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/connection_registry.py +18 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/messenger/close_connection_middleware.py +3 -4
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/messenger/open_transaction_logger_middleware.py +20 -12
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/_environment.py +2 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/executed_migration.py +3 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/migrations_config.py +3 -1
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/migrator.py +19 -6
- xtr_orm-3.0.0/src/xtr_orm/recipe/__init__.py +15 -0
- xtr_orm-3.0.0/src/xtr_orm/recipe/files/config/orm.py.tmpl +24 -0
- xtr_orm-3.0.0/src/xtr_orm/recipe/manifest.toml +32 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/LICENSE +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/bundle/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/bundle/orm_config.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/command/connections.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/database/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/database/database_manager.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/invalid_argument_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/migration_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/orm_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/session_unavailable_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/unknown_connection_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/exception/unsupported_database_error.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/messenger/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/messenger/transaction_middleware.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/__init__.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/_history.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/_revisions.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/_schema.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/available_migration.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/direction.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/execution_result.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/migration_plan.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/migration_status.py +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/migrations/template/script.py.mako +0 -0
- {xtr_orm-1.5.0 → xtr_orm-3.0.0}/src/xtr_orm/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtr-orm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection.
|
|
5
5
|
Keywords: orm,database,migrations,sqlalchemy,alembic,asyncio
|
|
6
6
|
Author: Xterr
|
|
@@ -20,11 +20,11 @@ Requires-Dist: advanced-alchemy>=1.11
|
|
|
20
20
|
Requires-Dist: alembic>=1.20
|
|
21
21
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.41
|
|
22
22
|
Requires-Dist: typing-extensions>=4.4
|
|
23
|
-
Requires-Dist: xtr-logging-contracts>=
|
|
24
|
-
Requires-Dist: xtr-console>=
|
|
25
|
-
Requires-Dist: xtr-dependency-injection>=
|
|
26
|
-
Requires-Dist: xtr-service-contracts>=
|
|
27
|
-
Requires-Dist: xtr-messenger>=
|
|
23
|
+
Requires-Dist: xtr-logging-contracts>=3.0,<4
|
|
24
|
+
Requires-Dist: xtr-console>=3.0,<4 ; extra == 'console'
|
|
25
|
+
Requires-Dist: xtr-dependency-injection>=3.0,<4 ; extra == 'di'
|
|
26
|
+
Requires-Dist: xtr-service-contracts>=3.0,<4 ; extra == 'di'
|
|
27
|
+
Requires-Dist: xtr-messenger>=3.0,<4 ; extra == 'messenger'
|
|
28
28
|
Requires-Dist: asyncmy>=0.2.9 ; extra == 'mysql'
|
|
29
29
|
Requires-Dist: asyncpg>=0.29 ; extra == 'postgres'
|
|
30
30
|
Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
|
|
@@ -152,7 +152,7 @@ the files, not its schema against your models; `diff` compares those.
|
|
|
152
152
|
|
|
153
153
|
| Field | Default | Meaning |
|
|
154
154
|
|---|---|---|
|
|
155
|
-
| `directory` | `"migrations"` | Where revision files are read and written |
|
|
155
|
+
| `directory` | `"migrations"` | Where revision files are read and written; a relative path is relative to the working directory — under a kernel, start it with `%kernel.project_dir%` |
|
|
156
156
|
| `version_table` / `history_table` | `"alembic_version"` / `"alembic_version_history"` | The two tables |
|
|
157
157
|
| `version_table_schema` | `None` | Their schema; the connection's default when `None` |
|
|
158
158
|
| `file_template` | `"{year}{month}{day}{hour}{minute}{second}_{rev}_{slug}"` | How a file is named; also `{epoch}` |
|
|
@@ -202,15 +202,19 @@ removing it undoes.
|
|
|
202
202
|
- **Install** — `uv add "xtr-orm[di,console,postgres]"`; `mysql` or `sqlite` instead of
|
|
203
203
|
`postgres` for those databases; drop `console` to go without the commands; add `messenger`
|
|
204
204
|
for the [message bus middleware](#message-bus-middleware).
|
|
205
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
|
|
206
|
+
*Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/orm.py` with the
|
|
207
|
+
`default` connection, and `DATABASE_URL` in `.env`. It prints the middleware step, which a recipe
|
|
208
|
+
cannot make for you.
|
|
205
209
|
- **Activate** — `OrmBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported from
|
|
206
210
|
`xtr_orm.bundle`.
|
|
207
211
|
- **Brings along** — the logging and console bundles, when those packages are installed.
|
|
208
212
|
With the messenger bundle active too, the [middleware](#message-bus-middleware) are
|
|
209
213
|
registered for a bus to list.
|
|
210
|
-
- **Configure** —
|
|
211
|
-
`DATABASE_URL`,
|
|
212
|
-
against the models advanced-alchemy registers.
|
|
213
|
-
|
|
214
|
+
- **Configure** — the recipe writes a starting `<app>/config/orm.py` with the `default`
|
|
215
|
+
connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
|
|
216
|
+
diffs against the models advanced-alchemy registers. Add connections, replicas and table
|
|
217
|
+
definitions to that `@configure` function returning `OrmConfig` — see
|
|
214
218
|
[Kernel / bundle](#kernel--bundle).
|
|
215
219
|
- **Environment** — `DATABASE_URL`, read when a connection is first used — engine, session,
|
|
216
220
|
migrator or command — not when the application boots. Its query may carry options; see
|
|
@@ -406,8 +410,8 @@ MessageBusConfig(
|
|
|
406
410
|
| Name | Class | Does | Arguments |
|
|
407
411
|
|---|---|---|---|
|
|
408
412
|
| `orm_transaction` | `TransactionMiddleware` | Runs every handler of a message in one transaction, committed once they all succeeded and rolled back otherwise; a failure drops the other handlers' handled stamps, so a retry runs them all again | `connection_name`, the default connection when left out |
|
|
409
|
-
| `orm_close_connection` | `CloseConnectionMiddleware` | Closes the connections once a worker handled a message it consumed, so an idle worker holds none — once the last, when it handles several at once | `connection_names`, every connection when left out |
|
|
410
|
-
| `orm_open_transaction_logger` | `OpenTransactionLoggerMiddleware` | Logs an error — to the application's logger when the logging bundle is active — when a handler began a transaction — `begin()`, `begin_nested()` — and left it open | `connection_names`, every connection when left out |
|
|
413
|
+
| `orm_close_connection` | `CloseConnectionMiddleware` | Closes the connections once a worker handled a message it consumed, so an idle worker holds none — once the last, when it handles several at once | `connection_names`, every connection in use — its engine built — when left out |
|
|
414
|
+
| `orm_open_transaction_logger` | `OpenTransactionLoggerMiddleware` | Logs an error — to the application's logger when the logging bundle is active — when a handler began a transaction — `begin()`, `begin_nested()` — and left it open, in a message that was handled | `connection_names`, every connection in use — its engine built — when left out |
|
|
411
415
|
|
|
412
416
|
- **One session per message.** The messenger bundle makes every message a unit of work, and a
|
|
413
417
|
scoped service — the session, a repository built on it — is the unit's: every handler of
|
|
@@ -463,6 +467,11 @@ Every error derives from `OrmError`.
|
|
|
463
467
|
|
|
464
468
|
## Known limitations
|
|
465
469
|
|
|
470
|
+
- **Migrations take no lock.** Two processes migrating one database at once — two deploys
|
|
471
|
+
starting together — both run the same revisions; run `orm:migrations:migrate` from one
|
|
472
|
+
place, before the application starts.
|
|
473
|
+
- **Applied times follow the database.** `executed_at` has a time zone on PostgreSQL and is
|
|
474
|
+
naive elsewhere: UTC on SQLite, the session's time zone on MySQL.
|
|
466
475
|
- **advanced-alchemy's model registry is process-wide.** Its `metadata_registry` maps a
|
|
467
476
|
`bind_key` to table definitions for the whole process, so two kernels in one process share
|
|
468
477
|
it. Give a connection its `metadata` explicitly to keep a kernel's diff to its own models.
|
|
@@ -113,7 +113,7 @@ the files, not its schema against your models; `diff` compares those.
|
|
|
113
113
|
|
|
114
114
|
| Field | Default | Meaning |
|
|
115
115
|
|---|---|---|
|
|
116
|
-
| `directory` | `"migrations"` | Where revision files are read and written |
|
|
116
|
+
| `directory` | `"migrations"` | Where revision files are read and written; a relative path is relative to the working directory — under a kernel, start it with `%kernel.project_dir%` |
|
|
117
117
|
| `version_table` / `history_table` | `"alembic_version"` / `"alembic_version_history"` | The two tables |
|
|
118
118
|
| `version_table_schema` | `None` | Their schema; the connection's default when `None` |
|
|
119
119
|
| `file_template` | `"{year}{month}{day}{hour}{minute}{second}_{rev}_{slug}"` | How a file is named; also `{epoch}` |
|
|
@@ -163,15 +163,19 @@ removing it undoes.
|
|
|
163
163
|
- **Install** — `uv add "xtr-orm[di,console,postgres]"`; `mysql` or `sqlite` instead of
|
|
164
164
|
`postgres` for those databases; drop `console` to go without the commands; add `messenger`
|
|
165
165
|
for the [message bus middleware](#message-bus-middleware).
|
|
166
|
+
- **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
|
|
167
|
+
*Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/orm.py` with the
|
|
168
|
+
`default` connection, and `DATABASE_URL` in `.env`. It prints the middleware step, which a recipe
|
|
169
|
+
cannot make for you.
|
|
166
170
|
- **Activate** — `OrmBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported from
|
|
167
171
|
`xtr_orm.bundle`.
|
|
168
172
|
- **Brings along** — the logging and console bundles, when those packages are installed.
|
|
169
173
|
With the messenger bundle active too, the [middleware](#message-bus-middleware) are
|
|
170
174
|
registered for a bus to list.
|
|
171
|
-
- **Configure** —
|
|
172
|
-
`DATABASE_URL`,
|
|
173
|
-
against the models advanced-alchemy registers.
|
|
174
|
-
|
|
175
|
+
- **Configure** — the recipe writes a starting `<app>/config/orm.py` with the `default`
|
|
176
|
+
connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
|
|
177
|
+
diffs against the models advanced-alchemy registers. Add connections, replicas and table
|
|
178
|
+
definitions to that `@configure` function returning `OrmConfig` — see
|
|
175
179
|
[Kernel / bundle](#kernel--bundle).
|
|
176
180
|
- **Environment** — `DATABASE_URL`, read when a connection is first used — engine, session,
|
|
177
181
|
migrator or command — not when the application boots. Its query may carry options; see
|
|
@@ -367,8 +371,8 @@ MessageBusConfig(
|
|
|
367
371
|
| Name | Class | Does | Arguments |
|
|
368
372
|
|---|---|---|---|
|
|
369
373
|
| `orm_transaction` | `TransactionMiddleware` | Runs every handler of a message in one transaction, committed once they all succeeded and rolled back otherwise; a failure drops the other handlers' handled stamps, so a retry runs them all again | `connection_name`, the default connection when left out |
|
|
370
|
-
| `orm_close_connection` | `CloseConnectionMiddleware` | Closes the connections once a worker handled a message it consumed, so an idle worker holds none — once the last, when it handles several at once | `connection_names`, every connection when left out |
|
|
371
|
-
| `orm_open_transaction_logger` | `OpenTransactionLoggerMiddleware` | Logs an error — to the application's logger when the logging bundle is active — when a handler began a transaction — `begin()`, `begin_nested()` — and left it open | `connection_names`, every connection when left out |
|
|
374
|
+
| `orm_close_connection` | `CloseConnectionMiddleware` | Closes the connections once a worker handled a message it consumed, so an idle worker holds none — once the last, when it handles several at once | `connection_names`, every connection in use — its engine built — when left out |
|
|
375
|
+
| `orm_open_transaction_logger` | `OpenTransactionLoggerMiddleware` | Logs an error — to the application's logger when the logging bundle is active — when a handler began a transaction — `begin()`, `begin_nested()` — and left it open, in a message that was handled | `connection_names`, every connection in use — its engine built — when left out |
|
|
372
376
|
|
|
373
377
|
- **One session per message.** The messenger bundle makes every message a unit of work, and a
|
|
374
378
|
scoped service — the session, a repository built on it — is the unit's: every handler of
|
|
@@ -424,6 +428,11 @@ Every error derives from `OrmError`.
|
|
|
424
428
|
|
|
425
429
|
## Known limitations
|
|
426
430
|
|
|
431
|
+
- **Migrations take no lock.** Two processes migrating one database at once — two deploys
|
|
432
|
+
starting together — both run the same revisions; run `orm:migrations:migrate` from one
|
|
433
|
+
place, before the application starts.
|
|
434
|
+
- **Applied times follow the database.** `executed_at` has a time zone on PostgreSQL and is
|
|
435
|
+
naive elsewhere: UTC on SQLite, the session's time zone on MySQL.
|
|
427
436
|
- **advanced-alchemy's model registry is process-wide.** Its `metadata_registry` maps a
|
|
428
437
|
`bind_key` to table definitions for the whole process, so two kernels in one process share
|
|
429
438
|
it. Give a connection its `metadata` explicitly to keep a kernel's diff to its own models.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-orm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
"alembic>=1.20",
|
|
31
31
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
32
32
|
"typing-extensions>=4.4",
|
|
33
|
-
"xtr-logging-contracts>=
|
|
33
|
+
"xtr-logging-contracts>=3.0,<4",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[[project.authors]]
|
|
@@ -42,15 +42,18 @@ postgres = ["asyncpg>=0.29"]
|
|
|
42
42
|
sqlite = ["aiosqlite>=0.20"]
|
|
43
43
|
mysql = ["asyncmy>=0.2.9"]
|
|
44
44
|
di = [
|
|
45
|
-
"xtr-dependency-injection>=
|
|
46
|
-
"xtr-service-contracts>=
|
|
45
|
+
"xtr-dependency-injection>=3.0,<4",
|
|
46
|
+
"xtr-service-contracts>=3.0,<4",
|
|
47
47
|
]
|
|
48
|
-
console = ["xtr-console>=
|
|
49
|
-
messenger = ["xtr-messenger>=
|
|
48
|
+
console = ["xtr-console>=3.0,<4"]
|
|
49
|
+
messenger = ["xtr-messenger>=3.0,<4"]
|
|
50
50
|
|
|
51
51
|
[project.entry-points."xtr_dependency_injection.bundles"]
|
|
52
52
|
orm = "xtr_orm.bundle:OrmBundle"
|
|
53
53
|
|
|
54
|
+
[project.entry-points.xtr_recipes]
|
|
55
|
+
orm = "xtr_orm.recipe"
|
|
56
|
+
|
|
54
57
|
[dependency-groups]
|
|
55
58
|
dev = [
|
|
56
59
|
"basedpyright>=1.21",
|
|
@@ -60,11 +63,11 @@ dev = [
|
|
|
60
63
|
"anyio>=4.0",
|
|
61
64
|
"ty>=0.0.83",
|
|
62
65
|
"aiosqlite>=0.20",
|
|
63
|
-
"xtr-console>=
|
|
64
|
-
"xtr-dependency-injection>=
|
|
65
|
-
"xtr-logging>=
|
|
66
|
-
"xtr-messenger>=
|
|
67
|
-
"xtr-service-contracts>=
|
|
66
|
+
"xtr-console>=3.0,<4",
|
|
67
|
+
"xtr-dependency-injection>=3.0,<4",
|
|
68
|
+
"xtr-logging>=3.0,<4",
|
|
69
|
+
"xtr-messenger>=3.0,<4",
|
|
70
|
+
"xtr-service-contracts>=3.0,<4",
|
|
68
71
|
]
|
|
69
72
|
|
|
70
73
|
[build-system]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-orm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ dependencies = [
|
|
|
26
26
|
"alembic>=1.20",
|
|
27
27
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
28
28
|
"typing-extensions>=4.4",
|
|
29
|
-
"xtr-logging-contracts>=
|
|
29
|
+
"xtr-logging-contracts>=3.0,<4",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
@@ -40,14 +40,14 @@ mysql = [
|
|
|
40
40
|
"asyncmy>=0.2.9",
|
|
41
41
|
]
|
|
42
42
|
di = [
|
|
43
|
-
"xtr-dependency-injection>=
|
|
44
|
-
"xtr-service-contracts>=
|
|
43
|
+
"xtr-dependency-injection>=3.0,<4",
|
|
44
|
+
"xtr-service-contracts>=3.0,<4",
|
|
45
45
|
]
|
|
46
46
|
console = [
|
|
47
|
-
"xtr-console>=
|
|
47
|
+
"xtr-console>=3.0,<4",
|
|
48
48
|
]
|
|
49
49
|
messenger = [
|
|
50
|
-
"xtr-messenger>=
|
|
50
|
+
"xtr-messenger>=3.0,<4",
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
# Advertises the bundle so debug:bundles can name it when installed but not listed.
|
|
@@ -55,6 +55,10 @@ messenger = [
|
|
|
55
55
|
[project.entry-points."xtr_dependency_injection.bundles"]
|
|
56
56
|
orm = "xtr_orm.bundle:OrmBundle"
|
|
57
57
|
|
|
58
|
+
# Advertises the recipe — what *Use in an application* takes — for recipes:sync.
|
|
59
|
+
[project.entry-points."xtr_recipes"]
|
|
60
|
+
orm = "xtr_orm.recipe"
|
|
61
|
+
|
|
58
62
|
[dependency-groups]
|
|
59
63
|
dev = [
|
|
60
64
|
"basedpyright>=1.21",
|
|
@@ -65,11 +69,11 @@ dev = [
|
|
|
65
69
|
"ty>=0.0.83",
|
|
66
70
|
# Every test runs on SQLite files under the test's own directory: no server.
|
|
67
71
|
"aiosqlite>=0.20",
|
|
68
|
-
"xtr-console>=
|
|
69
|
-
"xtr-dependency-injection>=
|
|
70
|
-
"xtr-logging>=
|
|
71
|
-
"xtr-messenger>=
|
|
72
|
-
"xtr-service-contracts>=
|
|
72
|
+
"xtr-console>=3.0,<4",
|
|
73
|
+
"xtr-dependency-injection>=3.0,<4",
|
|
74
|
+
"xtr-logging>=3.0,<4",
|
|
75
|
+
"xtr-messenger>=3.0,<4",
|
|
76
|
+
"xtr-service-contracts>=3.0,<4",
|
|
73
77
|
]
|
|
74
78
|
|
|
75
79
|
[build-system]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xtr-orm
|
|
3
|
+
description: How to configure database connections, inject async sessions and run versioned schema migrations with xtr-orm. Use when application code needs an AsyncEngine, an AsyncSession, a session per request or per message, read replicas, a DATABASE_URL, a new migration revision, a schema diff against models, the orm:migrations:* or orm:database:* console commands, one transaction per message on a bus, or when adding OrmBundle to an application on xtr-dependency-injection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xtr-orm
|
|
7
|
+
|
|
8
|
+
A connection is configuration. You name each database once — URL, replicas, engine and session
|
|
9
|
+
options, where its revisions live — and the bundle hands out the engine, the session maker, the
|
|
10
|
+
session of the current unit of work, a `Migrator` and a `DatabaseManager`, each qualified by the
|
|
11
|
+
connection's name. Everything is async; there is no sync counterpart.
|
|
12
|
+
|
|
13
|
+
## Quick reference
|
|
14
|
+
|
|
15
|
+
- Inject `Injected[AsyncSession]` for the default connection;
|
|
16
|
+
`Annotated[AsyncSession, Target("reports")]` for another. Never build a session yourself.
|
|
17
|
+
- Name databases in `<app>/config/orm.py` with a `@configure` function returning `OrmConfig`.
|
|
18
|
+
- Set `DATABASE_URL` with an async driver: `postgresql+asyncpg://`, `mysql+asyncmy://`,
|
|
19
|
+
`sqlite+aiosqlite:///`. Engine and session options may ride on its query.
|
|
20
|
+
- Write a revision from your models: `orm:migrations:diff --message "add invoices"`. Apply:
|
|
21
|
+
`orm:migrations:migrate`. Read it: `orm:migrations:status`.
|
|
22
|
+
- Without a container: `Migrator(engine, MigrationsConfig(directory="migrations"), Base.metadata)`.
|
|
23
|
+
- One transaction per message: list `orm_transaction` in the bus's `middleware`; handlers never
|
|
24
|
+
commit.
|
|
25
|
+
- On SQLite, set `MigrationsConfig(render_as_batch=True)` or table alterations cannot be written.
|
|
26
|
+
|
|
27
|
+
## Inject a session
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from typing import Annotated
|
|
31
|
+
|
|
32
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
33
|
+
from xtr_dependency_injection import Injected, Target, as_service
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@as_service
|
|
37
|
+
class InvoiceRepository:
|
|
38
|
+
def __init__(self, session: Injected[AsyncSession]) -> None:
|
|
39
|
+
self._session = session
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def report(
|
|
43
|
+
session: Annotated[AsyncSession, Target("reports")],
|
|
44
|
+
engine: Annotated[AsyncEngine, Target("reports")],
|
|
45
|
+
) -> None: ...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Per connection, qualified by name — and unqualified for the default one:
|
|
49
|
+
|
|
50
|
+
| Service | What it is |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `AsyncSession` | The session of the current unit of work: a request, a message, a command. Closed, and rolled back when not committed, as the unit ends |
|
|
53
|
+
| `AsyncEngine` | The engine on the primary database. Disposed when the container closes |
|
|
54
|
+
| `async_sessionmaker[AsyncSession]` | The session maker; `RoutingAsyncSessionMaker` when the connection has replicas |
|
|
55
|
+
| `SQLAlchemyAsyncConfig` | advanced-alchemy's configuration, for its repositories and services |
|
|
56
|
+
| `Migrator` / `DatabaseManager` | The connection's revisions / its database, both on the primary |
|
|
57
|
+
| `ConnectionRegistry` | Every connection by name, unqualified — what the commands use |
|
|
58
|
+
|
|
59
|
+
A session is scoped to the unit of work: asking for one outside any raises
|
|
60
|
+
`SessionUnavailableError`. Do not cache a session on a singleton.
|
|
61
|
+
|
|
62
|
+
## Migrations
|
|
63
|
+
|
|
64
|
+
`Migrator` is the whole surface. Every method opens its own connection; `generate` reads files
|
|
65
|
+
only.
|
|
66
|
+
|
|
67
|
+
| Call | Does |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `await migrator.status()` | `current`, `latest`, `previous`, `next`, `available`, `executed`, `new`, `executed_unavailable`, `is_up_to_date` |
|
|
70
|
+
| `await migrator.migrate(target)` | Runs every revision to `target`; returns `ExecutionResult`s. `plan(target)` plans it, `migrate_sql(target)` writes the SQL |
|
|
71
|
+
| `await migrator.execute(versions, Direction.UP)` | Exactly those revisions, nothing before or after. `plan_for_versions(versions, direction)` and `execute_sql(...)` alongside |
|
|
72
|
+
| `migrator.generate("message")` | Writes an empty revision after the latest (not awaited) |
|
|
73
|
+
| `await migrator.diff("message")` | Writes what the table definitions add; takes `allow_empty=`, `from_empty_schema=` |
|
|
74
|
+
| `await migrator.dump_schema("message", table_filters=...)` | A first revision creating every table an existing database has |
|
|
75
|
+
| `await migrator.add_versions(v)` / `add_all_versions()` | Records revisions as applied, running nothing |
|
|
76
|
+
| `await migrator.delete_versions(v)` / `delete_all_versions()` | Records them as not applied |
|
|
77
|
+
| `await migrator.rollup()` | Forgets every applied revision, then records the single remaining one |
|
|
78
|
+
|
|
79
|
+
A **target** is a revision id (full or partial), a relative one (`+1`, `-2`, `ae10+1`), or
|
|
80
|
+
`latest`, `first`, `next`, `prev`, `current`. A revision already applied is migrated *down* to.
|
|
81
|
+
|
|
82
|
+
Two tables, in one schema: `alembic_version` holds where the database **is** and decides what a
|
|
83
|
+
migration runs; `alembic_version_history` holds a row per revision ever applied, with
|
|
84
|
+
`executed_at` and `execution_time` in milliseconds.
|
|
85
|
+
|
|
86
|
+
`execute` is refused before touching the database unless the version table stays truthful: going
|
|
87
|
+
up a revision must not be applied and must follow only applied ones; going down it must be applied
|
|
88
|
+
and followed by none that is. **Up to date** compares the database's versions with the revision
|
|
89
|
+
files, not its schema with your models; `diff` does the latter.
|
|
90
|
+
|
|
91
|
+
Without a container:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
95
|
+
from xtr_orm import MigrationsConfig, Migrator
|
|
96
|
+
|
|
97
|
+
from app.models import Base
|
|
98
|
+
|
|
99
|
+
engine = create_async_engine("sqlite+aiosqlite:///shop.db")
|
|
100
|
+
migrator = Migrator(
|
|
101
|
+
engine,
|
|
102
|
+
MigrationsConfig(directory="migrations", render_as_batch=True),
|
|
103
|
+
Base.metadata,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
revision = await migrator.diff("add invoices") # AvailableMigration, or None with nothing to do
|
|
107
|
+
await migrator.migrate()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`MigrationsConfig` fields you reach for most: `directory` (relative to the working directory —
|
|
111
|
+
under a kernel start it with `%kernel.project_dir%`), `render_as_batch` (SQLite),
|
|
112
|
+
`compare_type` / `compare_server_default` (off by default), `version_table` / `history_table`,
|
|
113
|
+
`file_template`, `post_write_hooks` (a formatter), `transaction_per_migration`,
|
|
114
|
+
`template_directory`, `user_module_prefix`. See [references/migrations.md](references/migrations.md)
|
|
115
|
+
for every field and for writing your own `script.py.mako`.
|
|
116
|
+
|
|
117
|
+
## Console commands
|
|
118
|
+
|
|
119
|
+
Needs the `console` extra. Every command takes `--connection NAME`, defaulting to
|
|
120
|
+
`OrmConfig.default_connection`. The ones that change the database ask first; `-n` answers yes.
|
|
121
|
+
|
|
122
|
+
| Command | Does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `orm:migrations:diff [--message M]` | Writes what the models add; the database must be up to date first |
|
|
125
|
+
| `orm:migrations:migrate [VERSION]` | Runs every revision to `VERSION`, `latest` by default |
|
|
126
|
+
| `orm:migrations:status` / `:list` / `:current` / `:latest` | Where the database stands |
|
|
127
|
+
| `orm:migrations:up-to-date` | Exits non-zero when revisions are not applied — for a deploy check |
|
|
128
|
+
| `orm:database:create` / `:drop --force` | The database itself |
|
|
129
|
+
| `orm:run-sql SQL` | Runs one statement, committed |
|
|
130
|
+
|
|
131
|
+
Nine more, and every flag: [references/commands.md](references/commands.md).
|
|
132
|
+
|
|
133
|
+
Deploy order: `orm:migrations:migrate` from **one** place, before the application starts —
|
|
134
|
+
migrations take no lock.
|
|
135
|
+
|
|
136
|
+
Without a container, hand the commands their connections once:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from xtr_orm import ConnectionRegistry, DatabaseManager
|
|
140
|
+
from xtr_orm.command import use_connections
|
|
141
|
+
|
|
142
|
+
registry = ConnectionRegistry()
|
|
143
|
+
registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
|
|
144
|
+
use_connections(registry)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Use in an application
|
|
148
|
+
|
|
149
|
+
`uv run xtr-recipes recipes:sync` applies the recipe shipped with this package: it lists `OrmBundle`,
|
|
150
|
+
writes a starting `config/orm.py` with the `default` connection, and `DATABASE_URL` in `.env`. That
|
|
151
|
+
is the steps below a recipe can do; the middleware it prints for you to add.
|
|
152
|
+
|
|
153
|
+
1. **Install** — `uv add "xtr-orm[di,console,postgres]"`; `mysql` or `sqlite` instead of
|
|
154
|
+
`postgres`; drop `console` to go without the commands; add `messenger` for the bus middleware.
|
|
155
|
+
2. **Activate** — `OrmBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`
|
|
156
|
+
(`from xtr_orm.bundle import OrmBundle`).
|
|
157
|
+
3. **Brings along** — the logging and console bundles when those packages are installed, each
|
|
158
|
+
optional. With the logging bundle active an `orm` channel is added and every migrator logs the
|
|
159
|
+
revisions it runs there.
|
|
160
|
+
4. **Configure** — optional. With no configuration there is one connection, `default`, reading
|
|
161
|
+
`DATABASE_URL`, revisions in `migrations` in the project directory, diffing against the models
|
|
162
|
+
advanced-alchemy registers.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# <app>/config/orm.py
|
|
166
|
+
from xtr_dependency_injection import configure, env
|
|
167
|
+
from xtr_orm.bundle import ConnectionConfig, MigrationsConfig, OrmConfig
|
|
168
|
+
|
|
169
|
+
from app.models import Base
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@configure
|
|
173
|
+
def orm() -> OrmConfig:
|
|
174
|
+
return OrmConfig(
|
|
175
|
+
connections={
|
|
176
|
+
"default": ConnectionConfig(
|
|
177
|
+
url=env("DATABASE_URL"),
|
|
178
|
+
replicas={"replica1": env("DATABASE_REPLICA_URL")},
|
|
179
|
+
engine_options={"pool_size": 10, "pool_pre_ping": True},
|
|
180
|
+
session_options={"expire_on_commit": False},
|
|
181
|
+
metadata=Base.metadata,
|
|
182
|
+
migrations=MigrationsConfig(directory="%kernel.project_dir%/migrations"),
|
|
183
|
+
),
|
|
184
|
+
"reports": ConnectionConfig(url=env("REPORTS_URL"), bind_key="reports"),
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
5. **Environment** — `DATABASE_URL`, read when a connection is first used (engine, session,
|
|
190
|
+
migrator or command), not at boot. Its query may carry options — see
|
|
191
|
+
[references/connections.md](references/connections.md).
|
|
192
|
+
6. **Use** — inject `AsyncSession`; run `orm:migrations:*` from the console.
|
|
193
|
+
7. **Ignore** — nothing: commit `migrations/`.
|
|
194
|
+
8. **Check** — `debug:bundles` shows `orm` as `listed` and `active`, and
|
|
195
|
+
`orm:migrations:status` reaches the database.
|
|
196
|
+
9. **Remove** — take the `orm_*` entries out of every bus's `middleware`, drop the `BUNDLES`
|
|
197
|
+
entry, delete `<app>/config/orm.py`, then `uv remove xtr-orm`.
|
|
198
|
+
|
|
199
|
+
`OrmConfig` has two fields: `default_connection` (`"default"`) and `connections`
|
|
200
|
+
(`{"default": ConnectionConfig()}`). Two connections may not keep their revisions in one
|
|
201
|
+
directory; a non-default connection defaults to `%kernel.project_dir%/migrations/<name>`. Every
|
|
202
|
+
`ConnectionConfig` field, replicas and URL options: [references/connections.md](references/connections.md).
|
|
203
|
+
|
|
204
|
+
## One transaction per message
|
|
205
|
+
|
|
206
|
+
With the `messenger` extra and both bundles active, three middleware are registered by name.
|
|
207
|
+
Nothing is added to a bus by itself — the bus lists what it wants, and order matters.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# <app>/config/messenger.py
|
|
211
|
+
MessageBusConfig(
|
|
212
|
+
middleware=[
|
|
213
|
+
"orm_close_connection", # outermost: closes after the transaction ended
|
|
214
|
+
"orm_transaction",
|
|
215
|
+
{"orm_transaction": {"connection_name": "reports"}},
|
|
216
|
+
"orm_open_transaction_logger", # inside orm_transaction
|
|
217
|
+
],
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Name | Class | Arguments |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `orm_transaction` | `TransactionMiddleware` | `connection_name`, the default connection when left out |
|
|
224
|
+
| `orm_close_connection` | `CloseConnectionMiddleware` | `connection_names`, every connection in use when left out |
|
|
225
|
+
| `orm_open_transaction_logger` | `OpenTransactionLoggerMiddleware` | `connection_names`, as above |
|
|
226
|
+
|
|
227
|
+
A handler asks for `Injected[AsyncSession]` like any service and adds rows; it never commits.
|
|
228
|
+
|
|
229
|
+
- Every handler of a message shares one session per connection. A message dispatched while
|
|
230
|
+
another is handled joins its unit of work, in a savepoint of its own.
|
|
231
|
+
- A follow-up dispatched to a transport goes out at once unless stamped:
|
|
232
|
+
`await bus.dispatch(SendReceipt(order.id), DispatchAfterCurrentBusStamp())` sends it only after
|
|
233
|
+
the commit, and never after a rollback.
|
|
234
|
+
- Only a message a worker consumed closes connections afterwards; `sync://` closes nothing.
|
|
235
|
+
- Find a dropped connection with the engine option `pool_pre_ping`, not a middleware.
|
|
236
|
+
|
|
237
|
+
More, including building the middleware without a container:
|
|
238
|
+
[references/messenger.md](references/messenger.md).
|
|
239
|
+
|
|
240
|
+
## Creating and dropping a database
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from xtr_orm import DatabaseManager
|
|
244
|
+
|
|
245
|
+
database = DatabaseManager("postgresql+asyncpg://app:secret@db/shop")
|
|
246
|
+
await database.exists()
|
|
247
|
+
await database.create(if_not_exists=True) # True when it created one
|
|
248
|
+
await database.drop(if_exists=True)
|
|
249
|
+
database.backend, database.database, database.safe_url # properties
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The server is reached through its maintenance database — `postgres`, `master`, none on MySQL and
|
|
253
|
+
MariaDB — carrying every connection parameter of the URL but the database name. A SQLite database
|
|
254
|
+
is its file. Any other server raises `UnsupportedDatabaseError`.
|
|
255
|
+
|
|
256
|
+
## Errors
|
|
257
|
+
|
|
258
|
+
Every error derives from `OrmError`.
|
|
259
|
+
|
|
260
|
+
| Error | Raised when |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `InvalidArgumentError` | A configuration cannot be used: an empty name, an unknown engine/session option, a URL option that does not read. Also a `ValueError` |
|
|
263
|
+
| `MigrationError` | A migration is refused or revisions cannot be read: an unknown or ambiguous version, a revision out of order, a diff of a database behind its revisions |
|
|
264
|
+
| `UnknownConnectionError` | A connection asked for by a name nothing registered. Also a `LookupError` |
|
|
265
|
+
| `UnsupportedDatabaseError` | A database created or dropped on a server whose statements are not known |
|
|
266
|
+
| `SessionUnavailableError` | A session asked for outside a unit of work, or of a connection registered without one |
|
|
267
|
+
|
|
268
|
+
## Do not
|
|
269
|
+
|
|
270
|
+
- Do not use a sync driver. `postgresql://` fails; write `postgresql+asyncpg://`.
|
|
271
|
+
- Do not commit in a handler under `orm_transaction`, and do not wrap one in
|
|
272
|
+
`async with session.begin()` — the transaction is already open and it is refused.
|
|
273
|
+
- Do not build engines or sessions by hand in application code, and do not hold a session past its
|
|
274
|
+
unit of work or store one on a singleton.
|
|
275
|
+
- Do not write an `env.py` or an `alembic.ini`; the package runs the revisions itself.
|
|
276
|
+
- Do not expect `up-to-date` to notice a model change — run `diff` — and do not run `diff` against
|
|
277
|
+
a database behind its revisions; migrate first.
|
|
278
|
+
- Do not leave `render_as_batch` off on SQLite.
|
|
279
|
+
- Do not run `orm:migrations:migrate` from several processes at once; it takes no lock.
|
|
280
|
+
- Do not rely on advanced-alchemy's process-wide model registry when two kernels share one
|
|
281
|
+
process: give each connection its `metadata`.
|
|
282
|
+
- Do not expect a timezone on `executed_at` outside PostgreSQL; it is naive elsewhere.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# The `orm:*` console commands
|
|
2
|
+
|
|
3
|
+
Needs the `console` extra: `uv add "xtr-orm[console]"`. With a container, the bundle declares them
|
|
4
|
+
when the console bundle is active, and each acts on the connections the bundle registered.
|
|
5
|
+
|
|
6
|
+
Every command takes `--connection NAME`; the default connection is used without it. The ones that
|
|
7
|
+
change the database ask first, and `-n` answers yes. `--message` names the revision and its file.
|
|
8
|
+
|
|
9
|
+
## The database
|
|
10
|
+
|
|
11
|
+
| Command | Flags |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `orm:database:create` | `--if-not-exists` — do nothing, successfully, when it exists |
|
|
14
|
+
| `orm:database:drop` | `--force` / `-f` — required, the confirmation that everything may be lost; `--if-exists` |
|
|
15
|
+
| `orm:run-sql SQL` | `--force-fetch` — show rows even when the statement does not look like it returns any. The statement is committed |
|
|
16
|
+
|
|
17
|
+
## Reading where the database stands
|
|
18
|
+
|
|
19
|
+
| Command | Shows |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `orm:migrations:current` | The revisions the version table holds; `base` for none |
|
|
22
|
+
| `orm:migrations:latest` | The revisions no other follows |
|
|
23
|
+
| `orm:migrations:list` | Every revision with its status, when it ran and how long it took |
|
|
24
|
+
| `orm:migrations:status` | The two tables, the database, previous / current / next / latest, and the counts |
|
|
25
|
+
| `orm:migrations:up-to-date` | Exits 1 with revisions not applied. `--fail-on-unregistered` / `-u` exits 2 for applied revisions whose file is gone; `--list-migrations` / `-l` names them |
|
|
26
|
+
|
|
27
|
+
`up-to-date` is the deploy check: it compares the database's versions with the revision files, not
|
|
28
|
+
its schema with your models.
|
|
29
|
+
|
|
30
|
+
## Running revisions
|
|
31
|
+
|
|
32
|
+
| Command | Flags |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `orm:migrations:migrate [VERSION]` | `VERSION` defaults to `latest`; also a full or partial id, a relative one (`+1`, `-2`), `first`, `next`, `prev`, `current`. `--dry-run` reports the plan; `--write-sql PATH` writes the SQL instead of running it; `--allow-no-migration` succeeds when there is nothing to run |
|
|
35
|
+
| `orm:migrations:execute VERSION...` | `--up` (the default) or `--down`; `--dry-run`; `--write-sql PATH`. Runs exactly those revisions and nothing before or after. Going up each must not be applied and must follow only applied ones; going down each must be applied and be followed by none that is |
|
|
36
|
+
|
|
37
|
+
`--write-sql` writes to the file named, or to a dated file in the directory named.
|
|
38
|
+
|
|
39
|
+
## Writing revisions
|
|
40
|
+
|
|
41
|
+
| Command | Flags |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `orm:migrations:generate` | `--message M`. An empty revision after the latest, to fill in by hand |
|
|
44
|
+
| `orm:migrations:diff` | `--message M`; `--allow-empty-diff` writes a revision even when nothing differs; `--from-empty-schema` writes every table as if the database were empty. The database must be at the latest revisions first |
|
|
45
|
+
| `orm:migrations:dump-schema` | `--message M`; `--filter-tables RE`, repeatable, dumps only the tables whose name the expression finds. For a database that predates its revisions, so there must be none yet. Written, not applied — follow with `orm:migrations:rollup` |
|
|
46
|
+
|
|
47
|
+
## Recording without running
|
|
48
|
+
|
|
49
|
+
| Command | Flags |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `orm:migrations:version [VERSION]` | Exactly one of `--add` / `--delete`; `--all` for every revision instead of one. Adding, the revision must follow only applied ones; deleting, no applied revision may follow it. An applied revision whose file is gone can always be deleted |
|
|
52
|
+
| `orm:migrations:rollup` | Forgets every applied revision, then records the single remaining one as all there is |
|
|
53
|
+
|
|
54
|
+
## Without a container
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from xtr_orm import ConnectionRegistry, DatabaseManager
|
|
58
|
+
from xtr_orm.command import use_connections
|
|
59
|
+
|
|
60
|
+
registry = ConnectionRegistry()
|
|
61
|
+
registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
|
|
62
|
+
use_connections(registry)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The command classes are exported from `xtr_orm.command` — `MigrationsMigrateCommand`,
|
|
66
|
+
`MigrationsDiffCommand`, `DatabaseCreateCommand` and the rest — for a console application to
|
|
67
|
+
register itself.
|