snake-orm 0.1.0b1__py3-none-any.whl
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.
- snake_orm-0.1.0b1.dist-info/METADATA +366 -0
- snake_orm-0.1.0b1.dist-info/RECORD +198 -0
- snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
- snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
- snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
- snakeorm/__init__.py +214 -0
- snakeorm/advisor.py +109 -0
- snakeorm/cli/__init__.py +3 -0
- snakeorm/cli/app.py +992 -0
- snakeorm/cli/discovery.py +179 -0
- snakeorm/cli/hooks.py +86 -0
- snakeorm/compiler/__init__.py +3 -0
- snakeorm/compiler/compiler.py +330 -0
- snakeorm/connection.py +228 -0
- snakeorm/contrib/__init__.py +17 -0
- snakeorm/contrib/asgi.py +192 -0
- snakeorm/contrib/config.py +264 -0
- snakeorm/contrib/deliver.py +256 -0
- snakeorm/contrib/django.py +245 -0
- snakeorm/contrib/sidecar.py +44 -0
- snakeorm/contrib/wsgi.py +176 -0
- snakeorm/core/__init__.py +1 -0
- snakeorm/core/config.py +173 -0
- snakeorm/core/converters.py +172 -0
- snakeorm/core/exceptions.py +175 -0
- snakeorm/core/placement.py +15 -0
- snakeorm/core/sentinels.py +37 -0
- snakeorm/core/signals.py +114 -0
- snakeorm/debug/__init__.py +42 -0
- snakeorm/debug/assets/css/panel.css +616 -0
- snakeorm/debug/assets/css/snake_orm_app.css +293 -0
- snakeorm/debug/assets/img/icon.svg +5 -0
- snakeorm/debug/assets/js/history.js +425 -0
- snakeorm/debug/assets/js/language.js +283 -0
- snakeorm/debug/assets/js/panel.js +241 -0
- snakeorm/debug/assets/js/snake_orm_app.js +82 -0
- snakeorm/debug/assets/pages/cli.html +28 -0
- snakeorm/debug/assets/pages/config.html +34 -0
- snakeorm/debug/assets/pages/database_first.html +29 -0
- snakeorm/debug/assets/pages/help.html +21 -0
- snakeorm/debug/assets/pages/history.html +27 -0
- snakeorm/debug/capture.py +234 -0
- snakeorm/debug/channel.py +131 -0
- snakeorm/debug/collector.py +144 -0
- snakeorm/debug/config.py +149 -0
- snakeorm/debug/html.py +578 -0
- snakeorm/debug/matrix.py +60 -0
- snakeorm/debug/origin.py +42 -0
- snakeorm/debug/otel/__init__.py +68 -0
- snakeorm/debug/otel/context.py +43 -0
- snakeorm/debug/otel/exporter.py +277 -0
- snakeorm/debug/otel/payload.py +97 -0
- snakeorm/debug/otel/report.py +29 -0
- snakeorm/debug/otel/spans.py +393 -0
- snakeorm/debug/otel/summary.py +59 -0
- snakeorm/debug/record.py +54 -0
- snakeorm/debug/report.py +311 -0
- snakeorm/debug/testing.py +23 -0
- snakeorm/decorators/__init__.py +13 -0
- snakeorm/decorators/abstract.py +26 -0
- snakeorm/decorators/db_first.py +104 -0
- snakeorm/decorators/function.py +28 -0
- snakeorm/decorators/model.py +268 -0
- snakeorm/decorators/polymorphic.py +140 -0
- snakeorm/decorators/result.py +167 -0
- snakeorm/decorators/row.py +90 -0
- snakeorm/decorators/trigger.py +47 -0
- snakeorm/decorators/view.py +149 -0
- snakeorm/dialects/__init__.py +6 -0
- snakeorm/dialects/base.py +373 -0
- snakeorm/dialects/capabilities.py +571 -0
- snakeorm/dialects/literals.py +46 -0
- snakeorm/dialects/matrix.py +384 -0
- snakeorm/dialects/mysql.py +519 -0
- snakeorm/dialects/postgres.py +504 -0
- snakeorm/dialects/sqlite.py +417 -0
- snakeorm/drivers/__init__.py +18 -0
- snakeorm/drivers/asyncbase.py +73 -0
- snakeorm/drivers/asyncdecorators.py +244 -0
- snakeorm/drivers/asyncpool.py +228 -0
- snakeorm/drivers/asyncpsycopg.py +124 -0
- snakeorm/drivers/asyncpymysql.py +50 -0
- snakeorm/drivers/asyncsqlite.py +30 -0
- snakeorm/drivers/base.py +76 -0
- snakeorm/drivers/failures.py +181 -0
- snakeorm/drivers/logging.py +167 -0
- snakeorm/drivers/pool.py +352 -0
- snakeorm/drivers/psycopg.py +188 -0
- snakeorm/drivers/pymysql.py +176 -0
- snakeorm/drivers/savepoints.py +46 -0
- snakeorm/drivers/sqlite.py +170 -0
- snakeorm/drivers/threaded.py +143 -0
- snakeorm/drivers/timeout.py +87 -0
- snakeorm/dto/__init__.py +55 -0
- snakeorm/dto/read.py +231 -0
- snakeorm/dto/region.py +326 -0
- snakeorm/dto/resolve.py +354 -0
- snakeorm/dto/spec.py +201 -0
- snakeorm/expressions/__init__.py +88 -0
- snakeorm/expressions/conditional.py +178 -0
- snakeorm/expressions/expression.py +810 -0
- snakeorm/expressions/functions.py +71 -0
- snakeorm/expressions/keys.py +299 -0
- snakeorm/expressions/paths.py +43 -0
- snakeorm/expressions/scalar.py +293 -0
- snakeorm/expressions/window.py +248 -0
- snakeorm/fields/__init__.py +68 -0
- snakeorm/fields/check.py +79 -0
- snakeorm/fields/column.py +316 -0
- snakeorm/fields/enum.py +64 -0
- snakeorm/fields/index.py +46 -0
- snakeorm/fields/relationship.py +738 -0
- snakeorm/fields/typed.py +736 -0
- snakeorm/helpers/__init__.py +1 -0
- snakeorm/helpers/annotations.py +21 -0
- snakeorm/helpers/inheritance.py +41 -0
- snakeorm/helpers/pyliteral.py +52 -0
- snakeorm/helpers/pytype.py +50 -0
- snakeorm/introspection/__init__.py +13 -0
- snakeorm/introspection/base.py +30 -0
- snakeorm/introspection/drift.py +78 -0
- snakeorm/introspection/models.py +633 -0
- snakeorm/introspection/mysql.py +367 -0
- snakeorm/introspection/postgres.py +397 -0
- snakeorm/introspection/sqlite.py +217 -0
- snakeorm/introspection/unsupported.py +94 -0
- snakeorm/linker/__init__.py +3 -0
- snakeorm/linker/linker.py +521 -0
- snakeorm/metadata/__init__.py +41 -0
- snakeorm/metadata/check.py +37 -0
- snakeorm/metadata/column.py +201 -0
- snakeorm/metadata/enum_storage.py +33 -0
- snakeorm/metadata/fk_action.py +19 -0
- snakeorm/metadata/foreign_key.py +26 -0
- snakeorm/metadata/index.py +43 -0
- snakeorm/metadata/index_method.py +19 -0
- snakeorm/metadata/int_size.py +20 -0
- snakeorm/metadata/json_storage.py +21 -0
- snakeorm/metadata/polymorphic.py +30 -0
- snakeorm/metadata/primary_key.py +23 -0
- snakeorm/metadata/relationship.py +60 -0
- snakeorm/metadata/relationship_kind.py +43 -0
- snakeorm/metadata/routine.py +26 -0
- snakeorm/metadata/server_default.py +20 -0
- snakeorm/metadata/table.py +97 -0
- snakeorm/metadata/table_kind.py +23 -0
- snakeorm/metadata/trigger.py +62 -0
- snakeorm/metadata/type_params.py +348 -0
- snakeorm/migration/__init__.py +88 -0
- snakeorm/migration/asyncrunner.py +168 -0
- snakeorm/migration/autodetect.py +96 -0
- snakeorm/migration/ddl.py +1172 -0
- snakeorm/migration/diff.py +701 -0
- snakeorm/migration/loader.py +77 -0
- snakeorm/migration/operations.py +952 -0
- snakeorm/migration/planner.py +78 -0
- snakeorm/migration/realize.py +420 -0
- snakeorm/migration/renames.py +153 -0
- snakeorm/migration/render.py +1095 -0
- snakeorm/migration/runner.py +314 -0
- snakeorm/migration/squash.py +60 -0
- snakeorm/migration/state.py +107 -0
- snakeorm/model.py +162 -0
- snakeorm/py.typed +0 -0
- snakeorm/query/__init__.py +9 -0
- snakeorm/query/compound.py +288 -0
- snakeorm/query/join_kind.py +19 -0
- snakeorm/query/joined.py +143 -0
- snakeorm/query/query.py +1001 -0
- snakeorm/query/recursive.py +244 -0
- snakeorm/registry/__init__.py +5 -0
- snakeorm/registry/by_module.py +163 -0
- snakeorm/registry/registry.py +166 -0
- snakeorm/session/__init__.py +8 -0
- snakeorm/session/asyncsession.py +638 -0
- snakeorm/session/coercion.py +306 -0
- snakeorm/session/factory.py +29 -0
- snakeorm/session/guards.py +269 -0
- snakeorm/session/isolation.py +23 -0
- snakeorm/session/mapper.py +213 -0
- snakeorm/session/planning.py +815 -0
- snakeorm/session/retry.py +96 -0
- snakeorm/session/session.py +871 -0
- snakeorm/session/shared.py +355 -0
- snakeorm/sql/__init__.py +18 -0
- snakeorm/sql/adapt.py +104 -0
- snakeorm/sql/aggregate.py +232 -0
- snakeorm/sql/condition.py +359 -0
- snakeorm/sql/delete.py +46 -0
- snakeorm/sql/insert.py +149 -0
- snakeorm/sql/joins.py +158 -0
- snakeorm/sql/pk_subquery.py +54 -0
- snakeorm/sql/refs.py +19 -0
- snakeorm/sql/resolver.py +28 -0
- snakeorm/sql/select.py +175 -0
- snakeorm/sql/update.py +82 -0
- snakeorm/sql/value.py +552 -0
- snakeorm/times.py +276 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Asynchronous migration runtime: same logic, the other execution seam.
|
|
2
|
+
|
|
3
|
+
SQL generation has no "colour" (`up_sql()` does not execute), so diff, DDL, `realize()` and the
|
|
4
|
+
operations are reused as they are; only who talks to the DB changes. The real difference: DATA
|
|
5
|
+
migrations do NOT run here — `RunPython` receives a SYNCHRONOUS `SnakeSession`, which would block the
|
|
6
|
+
event loop — and it stops dead rather than marking them applied with the data never executed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from snakeorm.core.placement import DEFAULT_SCHEMA
|
|
12
|
+
from snakeorm.dialects import SnakeDialect
|
|
13
|
+
from snakeorm.drivers.asyncbase import AsyncDriver
|
|
14
|
+
from snakeorm.core.exceptions import SnakeMigrationError
|
|
15
|
+
from snakeorm.migration.operations import (
|
|
16
|
+
SnakeDataOperation,
|
|
17
|
+
SnakeMigrationOperation,
|
|
18
|
+
SnakeOperation,
|
|
19
|
+
)
|
|
20
|
+
from snakeorm.migration.realize import realize
|
|
21
|
+
from snakeorm.migration.runner import (
|
|
22
|
+
Migration,
|
|
23
|
+
explain_rebuild_failure,
|
|
24
|
+
squash_already_done,
|
|
25
|
+
tracking_table_ddl,
|
|
26
|
+
)
|
|
27
|
+
from snakeorm.sql.refs import qualified
|
|
28
|
+
|
|
29
|
+
_TRACKING_TABLE = "snake_migrations"
|
|
30
|
+
_SCHEMA = DEFAULT_SCHEMA
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AsyncMigrationRunner:
|
|
34
|
+
"""Applies/reverts migrations over an `AsyncDriver`, with the same tracking as the sync one."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, driver: AsyncDriver, dialect: SnakeDialect) -> None:
|
|
37
|
+
self._driver = driver
|
|
38
|
+
self._dialect = dialect
|
|
39
|
+
|
|
40
|
+
async def ensure_tracking_table(self) -> None:
|
|
41
|
+
"""Creates the tracking table if it does not exist (idempotent)."""
|
|
42
|
+
await self._driver.execute(tracking_table_ddl(self._dialect), ())
|
|
43
|
+
await self._driver.commit()
|
|
44
|
+
|
|
45
|
+
async def applied_versions(self) -> set[str]:
|
|
46
|
+
"""Returns the set of versions already applied."""
|
|
47
|
+
version = self._dialect.quote_ident("version")
|
|
48
|
+
rows = await self._driver.fetch_all(
|
|
49
|
+
f"SELECT {version} FROM {self._table_ref()}", ()
|
|
50
|
+
)
|
|
51
|
+
return {str(row[0]) for row in rows}
|
|
52
|
+
|
|
53
|
+
async def apply(self, migrations: list[Migration]) -> list[str]:
|
|
54
|
+
"""Applies the pending migrations in order. Returns the versions applied just now.
|
|
55
|
+
|
|
56
|
+
Same semantics as the synchronous one: idempotent, atomic per migration with transactional
|
|
57
|
+
DDL, and a hard stop if a squash replaces a history that was applied HALFWAY.
|
|
58
|
+
"""
|
|
59
|
+
await self.ensure_tracking_table()
|
|
60
|
+
already = await self.applied_versions()
|
|
61
|
+
replaced = {
|
|
62
|
+
version for migration in migrations for version in migration.replaces
|
|
63
|
+
}
|
|
64
|
+
newly: list[str] = []
|
|
65
|
+
for migration in migrations:
|
|
66
|
+
if migration.version in already or migration.version in replaced:
|
|
67
|
+
continue
|
|
68
|
+
if migration.replaces and squash_already_done(migration, already):
|
|
69
|
+
await self._record(migration.version)
|
|
70
|
+
await self._driver.commit()
|
|
71
|
+
newly.append(migration.version)
|
|
72
|
+
continue
|
|
73
|
+
if self._dialect.supports_transactional_ddl:
|
|
74
|
+
await self._apply_atomic(migration)
|
|
75
|
+
else:
|
|
76
|
+
await self._apply_stepwise(migration)
|
|
77
|
+
newly.append(migration.version)
|
|
78
|
+
return newly
|
|
79
|
+
|
|
80
|
+
async def rollback(self, migration: Migration) -> None:
|
|
81
|
+
"""Reverts a migration (operations in reverse order) and deletes its record."""
|
|
82
|
+
for operation in reversed(realize(migration.operations, self._dialect)):
|
|
83
|
+
await self._backward(operation)
|
|
84
|
+
await self._unrecord(migration.version)
|
|
85
|
+
await self._driver.commit()
|
|
86
|
+
|
|
87
|
+
async def _apply_atomic(self, migration: Migration) -> None:
|
|
88
|
+
"""Applies a migration wrapped in the engine's transaction: all or nothing."""
|
|
89
|
+
plan = realize(migration.operations, self._dialect)
|
|
90
|
+
try:
|
|
91
|
+
for operation in plan:
|
|
92
|
+
await self._forward(operation)
|
|
93
|
+
await self._record(migration.version)
|
|
94
|
+
await self._driver.commit()
|
|
95
|
+
except Exception as error:
|
|
96
|
+
await self._driver.rollback()
|
|
97
|
+
# The SAME sentence the synchronous runner gives, out of the same function: a rebuild
|
|
98
|
+
# that the deferred keys could not cover fails identically here, and this repository has
|
|
99
|
+
# already paid for one complaint told two ways.
|
|
100
|
+
explained = explain_rebuild_failure(plan, self._dialect, error)
|
|
101
|
+
if explained is not None:
|
|
102
|
+
raise explained from error
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
async def _apply_stepwise(self, migration: Migration) -> None:
|
|
106
|
+
"""Applies operation by operation (engine without transactional DDL); on error, reports how many."""
|
|
107
|
+
applied = 0
|
|
108
|
+
plan = realize(migration.operations, self._dialect)
|
|
109
|
+
try:
|
|
110
|
+
for operation in plan:
|
|
111
|
+
await self._forward(operation)
|
|
112
|
+
applied += 1
|
|
113
|
+
await self._record(migration.version)
|
|
114
|
+
await self._driver.commit()
|
|
115
|
+
except SnakeMigrationError:
|
|
116
|
+
raise
|
|
117
|
+
except Exception as error:
|
|
118
|
+
raise SnakeMigrationError(
|
|
119
|
+
f"Migration '{migration.version}' failed after applying {applied} of "
|
|
120
|
+
f"{len(plan)} operation(s): {error}. The engine does not support "
|
|
121
|
+
f"transactional DDL, so the database was left half-way: review it and "
|
|
122
|
+
f"fix it by hand."
|
|
123
|
+
) from error
|
|
124
|
+
|
|
125
|
+
async def _forward(self, operation: SnakeMigrationOperation) -> None:
|
|
126
|
+
"""Emits a schema operation's DDL; a data one cannot be executed here."""
|
|
127
|
+
_reject_data_operation(operation)
|
|
128
|
+
if isinstance(operation, SnakeOperation):
|
|
129
|
+
for sql in operation.up_sql(self._dialect):
|
|
130
|
+
await self._driver.execute(sql, ())
|
|
131
|
+
|
|
132
|
+
async def _backward(self, operation: SnakeMigrationOperation) -> None:
|
|
133
|
+
"""Emits a schema operation's reverse DDL."""
|
|
134
|
+
_reject_data_operation(operation)
|
|
135
|
+
if isinstance(operation, SnakeOperation):
|
|
136
|
+
for sql in operation.down_sql(self._dialect):
|
|
137
|
+
await self._driver.execute(sql, ())
|
|
138
|
+
|
|
139
|
+
def _table_ref(self) -> str:
|
|
140
|
+
"""Reference to the tracking table, qualified only if the engine has schemas."""
|
|
141
|
+
return qualified(_SCHEMA, _TRACKING_TABLE, self._dialect)
|
|
142
|
+
|
|
143
|
+
async def _record(self, version: str) -> None:
|
|
144
|
+
column = self._dialect.quote_ident("version")
|
|
145
|
+
await self._driver.execute(
|
|
146
|
+
f"INSERT INTO {self._table_ref()} ({column}) "
|
|
147
|
+
f"VALUES ({self._dialect.placeholder(1)})",
|
|
148
|
+
(version,),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def _unrecord(self, version: str) -> None:
|
|
152
|
+
column = self._dialect.quote_ident("version")
|
|
153
|
+
await self._driver.execute(
|
|
154
|
+
f"DELETE FROM {self._table_ref()} WHERE {column} = {self._dialect.placeholder(1)}",
|
|
155
|
+
(version,),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _reject_data_operation(operation: SnakeMigrationOperation) -> None:
|
|
160
|
+
"""Stops dead at a DATA migration: skipping it would mark it applied with the data never run,
|
|
161
|
+
and that never raises an error, only wrong rows months later."""
|
|
162
|
+
if isinstance(operation, SnakeDataOperation):
|
|
163
|
+
raise SnakeMigrationError(
|
|
164
|
+
"The async runner cannot execute a data operation (`RunPython`): its "
|
|
165
|
+
"`forward`/`backward` take a SYNCHRONOUS `SnakeSession`, and running it here would "
|
|
166
|
+
"block the event loop for the whole migration. Apply that migration with "
|
|
167
|
+
"`MigrationRunner` (synchronous) over a synchronous driver; the schema ones do go here."
|
|
168
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Django-style autogen: rebuilds the state by replaying migrations and diffs it.
|
|
2
|
+
|
|
3
|
+
`replay` applies the operations of every migration onto an empty SchemaState -> the state the
|
|
4
|
+
schema SHOULD have according to the history. `autodetect` diffs that against the current
|
|
5
|
+
metadata (the code-first source of truth) and returns the operations for the new migration.
|
|
6
|
+
Neither snapshots nor reflection: the migration history IS the record of the state.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
|
|
13
|
+
from snakeorm.metadata import SnakeRoutineInfo, SnakeTableInfo, SnakeTriggerInfo
|
|
14
|
+
from snakeorm.migration.diff import diff_routines, diff_schema, diff_triggers
|
|
15
|
+
from snakeorm.migration.operations import SnakeOperation
|
|
16
|
+
from snakeorm.migration.runner import Migration
|
|
17
|
+
from snakeorm.migration.state import SchemaState
|
|
18
|
+
from snakeorm.registry import SnakeRegistry, registry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def current_schema(
|
|
22
|
+
reg: SnakeRegistry = registry,
|
|
23
|
+
*,
|
|
24
|
+
database: str | None = None,
|
|
25
|
+
include_unmanaged: bool = False,
|
|
26
|
+
) -> list[SnakeTableInfo]:
|
|
27
|
+
"""The current metadata (code-first source of truth): the tables of the registered models.
|
|
28
|
+
|
|
29
|
+
Excludes MIRROR models (`@snake_db_first`): they are not a source of truth, and autogen must not
|
|
30
|
+
touch them. With `database`, only those of THAT connection (which avoids creating every table in
|
|
31
|
+
every DB). `include_unmanaged=True` returns them too: drift detection uses it (comparing the
|
|
32
|
+
mirror against the DB), but MIGRATIONS never touch them (the default behaviour).
|
|
33
|
+
"""
|
|
34
|
+
return [
|
|
35
|
+
table
|
|
36
|
+
for model in reg.models()
|
|
37
|
+
if (table := reg.table_of(model)) is not None
|
|
38
|
+
and (include_unmanaged or table.is_managed)
|
|
39
|
+
and (database is None or table.database == database)
|
|
40
|
+
# A polymorphic CHILD contributes no table: it shares its base's (which already carries
|
|
41
|
+
# the whole hierarchy after `snake_link()`). Including it would duplicate the `CREATE TABLE`
|
|
42
|
+
# and produce phantom drift on every `makemigrations`.
|
|
43
|
+
and not table.is_polymorphic_child
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def current_routines(reg: SnakeRegistry = registry) -> list[SnakeRoutineInfo]:
|
|
48
|
+
"""The desired routines (code-first source of truth): those declared with `snake_function`."""
|
|
49
|
+
return list(reg.routines())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def current_triggers(reg: SnakeRegistry = registry) -> list[SnakeTriggerInfo]:
|
|
53
|
+
"""The DESIRED triggers: those declared with `snake_trigger(...)`."""
|
|
54
|
+
return list(reg.triggers())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def replay(migrations: Iterable[Migration]) -> SchemaState:
|
|
58
|
+
"""Rebuilds the schema state by applying the migrations' operations in order."""
|
|
59
|
+
state = SchemaState()
|
|
60
|
+
for migration in migrations:
|
|
61
|
+
for operation in migration.operations:
|
|
62
|
+
operation.apply_to_state(state)
|
|
63
|
+
return state
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def autodetect(
|
|
67
|
+
migrations: Iterable[Migration],
|
|
68
|
+
current: Iterable[SnakeTableInfo],
|
|
69
|
+
routines: Iterable[SnakeRoutineInfo] | None = None,
|
|
70
|
+
triggers: Iterable[SnakeTriggerInfo] | None = None,
|
|
71
|
+
) -> list[SnakeOperation]:
|
|
72
|
+
"""Diffs the state replayed from the history against the current metadata -> new operations.
|
|
73
|
+
|
|
74
|
+
Resolves FK targets by model name through the global registry. Global order: tables -> FKs ->
|
|
75
|
+
views (topological) -> functions -> triggers (each one depends on the ones before it).
|
|
76
|
+
"""
|
|
77
|
+
state = replay(migrations)
|
|
78
|
+
desired_routines = current_routines() if routines is None else list(routines)
|
|
79
|
+
# The state's triggers go in with the tables, and NOT because they are diffed here — that is
|
|
80
|
+
# `diff_triggers`, further down. A `RebuildTable` on an engine without `ALTER TABLE ADD
|
|
81
|
+
# CONSTRAINT` drops the table it remakes, which takes its triggers with it, and the operation
|
|
82
|
+
# has to carry them because `SnakeTableInfo` has no field for one. This is the only caller that
|
|
83
|
+
# has them, so this is where they are handed over.
|
|
84
|
+
schema_ops = diff_schema(
|
|
85
|
+
state.tables(),
|
|
86
|
+
current,
|
|
87
|
+
registry.table_by_name,
|
|
88
|
+
registry.table_by_qualified,
|
|
89
|
+
triggers=state.triggers(),
|
|
90
|
+
)
|
|
91
|
+
routine_ops = diff_routines(state.routines(), desired_routines)
|
|
92
|
+
# TRIGGERS go last: a Postgres one executes a FUNCTION, which has to exist beforehand.
|
|
93
|
+
trigger_ops = diff_triggers(
|
|
94
|
+
state.triggers(), current_triggers() if triggers is None else list(triggers)
|
|
95
|
+
)
|
|
96
|
+
return [*schema_ops, *routine_ops, *trigger_ops]
|