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,213 @@
|
|
|
1
|
+
"""Row hydration: the HOTTEST path of the ORM, with the plan compiled ONCE per table.
|
|
2
|
+
|
|
3
|
+
Which attribute, which converter (or none) and in which position is FIXED per table, so it gets
|
|
4
|
+
compiled once and reused instead of being resolved per row. It writes straight into the descriptor's
|
|
5
|
+
storage key WITHOUT calling the constructor (exactly what its `__set__` does): validating arguments
|
|
6
|
+
and applying defaults is pointless work when the values come from the database itself.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
from snakeorm.registry import registry_of
|
|
15
|
+
from snakeorm.metadata import SnakeTableInfo
|
|
16
|
+
from snakeorm.core.sentinels import NOT_LOADED
|
|
17
|
+
from snakeorm.session.coercion import converter_for
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
_Instruction = tuple[str, Callable[[object], object] | None]
|
|
22
|
+
"""(storage key, converter, or None if the value passes through as is)."""
|
|
23
|
+
|
|
24
|
+
_Dispatch = tuple[int, dict[str, type]] | None
|
|
25
|
+
"""(position of the discriminator in the row, value -> subclass). `None` on a normal table.
|
|
26
|
+
|
|
27
|
+
Resolved HERE and not per row: the position and the map are fixed per table (resolving them per row
|
|
28
|
+
cost 55% more on the hot path).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_Entry = tuple[SnakeTableInfo, tuple[_Instruction, ...], tuple[int, ...], _Dispatch]
|
|
32
|
+
"""(table that generated it, instructions, PK positions, polymorphic dispatch)."""
|
|
33
|
+
|
|
34
|
+
_CACHE: dict[tuple[type, type], _Entry] = {}
|
|
35
|
+
"""Compiled plan, indexed by (model being INSTANTIATED, model whose TABLE was read).
|
|
36
|
+
|
|
37
|
+
It is indexed by models (not by `id(table)`, which leaked: every `snake_link()` creates new tables).
|
|
38
|
+
The second element tells polymorphism apart, where `Dog` is hydrated from TWO row shapes (its own
|
|
39
|
+
and `Animal`'s). The table is stored TOO and the REFERENCE is compared, so a relink invalidates the
|
|
40
|
+
old plan.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_DISCARD = ""
|
|
44
|
+
"""Storage key of a column the row carries and this class does NOT have.
|
|
45
|
+
|
|
46
|
+
It happens under polymorphism: `Animal`'s row includes `Cat`'s `lives` column, and when hydrating a
|
|
47
|
+
`Dog` that value goes nowhere; writing it would leave a phantom attribute, so it gets thrown away.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _compile(
|
|
52
|
+
table: SnakeTableInfo, own: frozenset[str] | None
|
|
53
|
+
) -> tuple[_Instruction, ...]:
|
|
54
|
+
"""Translates the columns into instructions: where to write and which conversion applies, if any.
|
|
55
|
+
|
|
56
|
+
`own` are the column names the class being instantiated really declares. With `None` — the normal
|
|
57
|
+
case — they are all assumed to be its own. Only polymorphism passes a set: there the row comes
|
|
58
|
+
from the BASE's table and carries columns of the sibling classes.
|
|
59
|
+
"""
|
|
60
|
+
return tuple(
|
|
61
|
+
(
|
|
62
|
+
f"__snake_{column.attr_name or column.name}"
|
|
63
|
+
if own is None or column.name in own
|
|
64
|
+
else _DISCARD,
|
|
65
|
+
converter_for(column.python_type, column.scale),
|
|
66
|
+
)
|
|
67
|
+
for column in table.columns
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _entry(model: type, table: SnakeTableInfo, owner: type) -> _Entry:
|
|
72
|
+
"""The entry for that pair, compiling it the first time and redoing it if the table changed.
|
|
73
|
+
|
|
74
|
+
Everything precomputed lives in ONE entry so it gets invalidated in one go (two caches = two
|
|
75
|
+
sources of truth).
|
|
76
|
+
"""
|
|
77
|
+
key = (model, owner)
|
|
78
|
+
cached = _CACHE.get(key)
|
|
79
|
+
if cached is not None and cached[0] is table:
|
|
80
|
+
return cached
|
|
81
|
+
names = [column.name for column in table.columns]
|
|
82
|
+
entry: _Entry = (
|
|
83
|
+
table,
|
|
84
|
+
_compile(table, _own_columns(model) if owner is not model else None),
|
|
85
|
+
tuple(names.index(pk.name) for pk in table.primary_key.columns),
|
|
86
|
+
_dispatch(table, names, model),
|
|
87
|
+
)
|
|
88
|
+
_CACHE[key] = entry
|
|
89
|
+
return entry
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _own_columns(model: type) -> frozenset[str] | None:
|
|
93
|
+
"""The column names THAT model declares, so its siblings' ones can be discarded."""
|
|
94
|
+
table = registry_of(model).table_of(model)
|
|
95
|
+
return None if table is None else frozenset(column.name for column in table.columns)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _dispatch(table: SnakeTableInfo, names: list[str], owner: type) -> _Dispatch:
|
|
99
|
+
"""Where the discriminator is and which class each value stands for. `None` if there is no hierarchy.
|
|
100
|
+
|
|
101
|
+
Only the BASE dispatches: querying a child already filters by its value. The map is frozen along
|
|
102
|
+
with the plan and invalidated by the table's reference, which is why linking is an explicit pass
|
|
103
|
+
at the end.
|
|
104
|
+
"""
|
|
105
|
+
if table.polymorphic is None or not table.polymorphic.is_base:
|
|
106
|
+
return None
|
|
107
|
+
if (
|
|
108
|
+
table.polymorphic.column not in names
|
|
109
|
+
): # pragma: no cover - the compiler guarantees it
|
|
110
|
+
return None
|
|
111
|
+
# The OWNER's registry, not the global one. Asking the global one for a model that lives in
|
|
112
|
+
# a private registry returns `{}`, and `{}` is falsy, so the dispatch simply switches off:
|
|
113
|
+
# every row of a polymorphic hierarchy hydrates as the BASE class, with no error anywhere.
|
|
114
|
+
# This is the one site where resolving in the wrong registry stops shouting and starts lying.
|
|
115
|
+
by_value = registry_of(owner).polymorphic_map(table)
|
|
116
|
+
return (names.index(table.polymorphic.column), by_value) if by_value else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def dispatch_for(model: type, table: SnakeTableInfo) -> _Dispatch:
|
|
120
|
+
"""The polymorphic dispatch of that table, resolved ONCE and cached along with the plan."""
|
|
121
|
+
return _entry(model, table, model)[3]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def plan_for(
|
|
125
|
+
model: type, table: SnakeTableInfo, owner: type | None = None
|
|
126
|
+
) -> tuple[_Instruction, ...]:
|
|
127
|
+
"""The hydration instructions of that model over that row shape."""
|
|
128
|
+
return _entry(model, table, owner if owner is not None else model)[1]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pk_positions(model: type, table: SnakeTableInfo) -> tuple[int, ...]:
|
|
132
|
+
"""Positions of the PK inside the row, computed ONCE (used by the "LEFT JOIN with no partner"
|
|
133
|
+
detection; resolving them per row was O(columns²) on the `include` path)."""
|
|
134
|
+
return _entry(model, table, model)[2]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def partial_plan_for(
|
|
138
|
+
model: type, table: SnakeTableInfo, columns: frozenset[str]
|
|
139
|
+
) -> tuple[tuple[_Instruction, ...], tuple[str, ...]]:
|
|
140
|
+
"""The plan for a row that carries only SOME columns, plus the keys to mark as not loaded.
|
|
141
|
+
|
|
142
|
+
It SLICES the full plan instead of compiling a second one, and that is the point: the two would
|
|
143
|
+
be two answers to "where does column N go", and the day one of them learns about a new converter
|
|
144
|
+
the other keeps the old one. The projection is emitted in the table's declaration order for
|
|
145
|
+
exactly this reason, so position `i` of the row is the `i`-th kept column.
|
|
146
|
+
|
|
147
|
+
The second half is what makes the feature safe. Every column left out has its storage key filled
|
|
148
|
+
with `NOT_LOADED`, so the descriptor can tell "you did not ask for this" from "this is `None`"
|
|
149
|
+
— without it a deferred column reads as its default and nobody finds out.
|
|
150
|
+
|
|
151
|
+
`_DISCARD` keys are skipped: they belong to a sibling class under polymorphism and writing the
|
|
152
|
+
sentinel there would leave a phantom attribute on an object that never had one.
|
|
153
|
+
"""
|
|
154
|
+
full = plan_for(model, table)
|
|
155
|
+
kept: list[_Instruction] = []
|
|
156
|
+
missing: list[str] = []
|
|
157
|
+
for instruction, column in zip(full, table.columns, strict=True):
|
|
158
|
+
if column.name in columns:
|
|
159
|
+
kept.append(instruction)
|
|
160
|
+
elif instruction[0] is not _DISCARD:
|
|
161
|
+
missing.append(instruction[0])
|
|
162
|
+
return tuple(kept), tuple(missing)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def hydrate_partial(
|
|
166
|
+
model: type[T],
|
|
167
|
+
plan: tuple[_Instruction, ...],
|
|
168
|
+
missing: tuple[str, ...],
|
|
169
|
+
row: Sequence[object],
|
|
170
|
+
) -> T:
|
|
171
|
+
"""Builds an instance from a partial row, marking every absent column as not loaded."""
|
|
172
|
+
instance = _hydrate_with_plan(model, plan, row)
|
|
173
|
+
write = object.__setattr__
|
|
174
|
+
for storage_key in missing:
|
|
175
|
+
write(instance, storage_key, NOT_LOADED)
|
|
176
|
+
return instance
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def hydrate(
|
|
180
|
+
model: type[T],
|
|
181
|
+
table: SnakeTableInfo,
|
|
182
|
+
row: Sequence[object],
|
|
183
|
+
owner: type | None = None,
|
|
184
|
+
) -> T:
|
|
185
|
+
"""Builds an instance with the row's values, in column order.
|
|
186
|
+
|
|
187
|
+
No kwargs, no constructor and no resolving converters: the plan already says where each value
|
|
188
|
+
goes and what to do to it. `owner` is the model whose TABLE generated the row; it only differs
|
|
189
|
+
from `model` under polymorphism (the query goes against `Animal` and each row is instantiated as
|
|
190
|
+
a `Dog` or a `Cat`).
|
|
191
|
+
"""
|
|
192
|
+
return _hydrate_with_plan(model, plan_for(model, table, owner), row)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _hydrate_with_plan(
|
|
196
|
+
model: type[T], plan: tuple[_Instruction, ...], row: Sequence[object]
|
|
197
|
+
) -> T:
|
|
198
|
+
"""The hydration loop with the plan ALREADY resolved: it hoists the resolution OUT of the
|
|
199
|
+
per-row loop (`plan_for`/`dispatch_for` are fixed per table). Whoever maps N rows resolves the
|
|
200
|
+
plan once."""
|
|
201
|
+
instance = object.__new__(model)
|
|
202
|
+
write = object.__setattr__
|
|
203
|
+
for (storage_key, converter), value in zip(plan, row, strict=True):
|
|
204
|
+
if storage_key is not _DISCARD:
|
|
205
|
+
# `or value is None`: a NULL passes through untouched, with no conversion
|
|
206
|
+
# (`_to_bool(None)` would give `False`, `_to_uuid(None)` would blow up). Inline, not in a
|
|
207
|
+
# closure: +132 ns/column.
|
|
208
|
+
write(
|
|
209
|
+
instance,
|
|
210
|
+
storage_key,
|
|
211
|
+
value if converter is None or value is None else converter(value),
|
|
212
|
+
)
|
|
213
|
+
return instance
|