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,178 @@
|
|
|
1
|
+
"""Conditionals inside the SQL: `CASE WHEN`, `COALESCE`, `NULLIF`.
|
|
2
|
+
|
|
3
|
+
They are VALUES, not conditions: a `CASE` returns a datum (it gets compared, projected, aggregated),
|
|
4
|
+
so it inherits from `SnakeValue`. The TYPE is pinned down by the constructors:
|
|
5
|
+
`snake_case((cond, 1), default=0)` is `SnakeValue[int]`, and projecting it types the tuple with no `Any`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, TypeVar, overload
|
|
12
|
+
|
|
13
|
+
from snakeorm.core.exceptions import SnakeEmitError
|
|
14
|
+
from snakeorm.expressions.expression import SnakeCondition, SnakeValue
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
# One branch of a CASE: if the condition holds, the value (a column or a literal).
|
|
19
|
+
SnakeCaseBranch = tuple[SnakeCondition, "SnakeValue[T] | T"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
23
|
+
class SnakeCase(SnakeValue[T]):
|
|
24
|
+
"""`CASE WHEN cond THEN value ... [ELSE default] END`.
|
|
25
|
+
|
|
26
|
+
The branches are evaluated in order and the first match wins (the order is meaningful). Without
|
|
27
|
+
a `default`, no `ELSE` is emitted (in SQL that already means NULL).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
branches: tuple[SnakeCaseBranch[T], ...]
|
|
31
|
+
default: SnakeValue[T] | T | None = None
|
|
32
|
+
has_default: bool = False
|
|
33
|
+
|
|
34
|
+
def paths(self) -> tuple[tuple[str, ...], ...]:
|
|
35
|
+
"""Paths of the columns it mentions (conditions, results and default), for the JOIN planner."""
|
|
36
|
+
from snakeorm.expressions.paths import condition_paths
|
|
37
|
+
|
|
38
|
+
collected: list[tuple[str, ...]] = []
|
|
39
|
+
for condition, result in self.branches:
|
|
40
|
+
collected.extend(condition_paths(condition))
|
|
41
|
+
if isinstance(result, SnakeValue):
|
|
42
|
+
collected.extend(result.paths())
|
|
43
|
+
if isinstance(self.default, SnakeValue):
|
|
44
|
+
collected.extend(self.default.paths())
|
|
45
|
+
return tuple(collected)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
49
|
+
class SnakeCoalesce(SnakeValue[T]):
|
|
50
|
+
"""`COALESCE(a, b, ...)`: the first argument that is not NULL."""
|
|
51
|
+
|
|
52
|
+
arguments: tuple[SnakeValue[T] | T, ...]
|
|
53
|
+
|
|
54
|
+
def paths(self) -> tuple[tuple[str, ...], ...]:
|
|
55
|
+
"""Paths of the arguments that are columns."""
|
|
56
|
+
return tuple(
|
|
57
|
+
path
|
|
58
|
+
for argument in self.arguments
|
|
59
|
+
if isinstance(argument, SnakeValue)
|
|
60
|
+
for path in argument.paths()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
65
|
+
class SnakeNullIf(SnakeValue[T]):
|
|
66
|
+
"""`NULLIF(value, sentinel)`: NULL when the two are equal, and the value otherwise.
|
|
67
|
+
|
|
68
|
+
`T` is the type of the RESULT —which carries the `| None` this node introduces— and not that of
|
|
69
|
+
the operands, so the fields are stored untyped. Pinning them to `T` would demand a
|
|
70
|
+
`SnakeValue[int | None]` where the caller has a `SnakeValue[int]`, and invariance refuses it.
|
|
71
|
+
Same shape as `SnakeCoalesce`, which stores its arguments the same way and for the same reason:
|
|
72
|
+
the constructor in this module is what pins the types down, which is where they belong.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
value: SnakeValue[Any]
|
|
76
|
+
sentinel: SnakeValue[Any] | Any
|
|
77
|
+
|
|
78
|
+
def paths(self) -> tuple[tuple[str, ...], ...]:
|
|
79
|
+
"""Paths of the value and, if it is one, of the sentinel."""
|
|
80
|
+
collected = list(self.value.paths())
|
|
81
|
+
if isinstance(self.sentinel, SnakeValue):
|
|
82
|
+
collected.extend(self.sentinel.paths())
|
|
83
|
+
return tuple(collected)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_NO_DEFAULT = object()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# The overloads split the `SnakeValue[T] | T` union: together it is AMBIGUOUS (when passing
|
|
90
|
+
# `SnakeValue[str]`, `T` could be `str` or `SnakeValue[str]`, and mypy resolves it to `Never`). Split
|
|
91
|
+
# apart, each one pins `T` down.
|
|
92
|
+
@overload
|
|
93
|
+
def snake_case(*branches: SnakeCaseBranch[T]) -> SnakeCase[T]: ...
|
|
94
|
+
@overload
|
|
95
|
+
def snake_case(
|
|
96
|
+
*branches: SnakeCaseBranch[T], default: SnakeValue[T]
|
|
97
|
+
) -> SnakeCase[T]: ...
|
|
98
|
+
@overload
|
|
99
|
+
def snake_case(*branches: SnakeCaseBranch[T], default: T) -> SnakeCase[T]: ...
|
|
100
|
+
def snake_case(
|
|
101
|
+
*branches: SnakeCaseBranch[T], default: SnakeValue[T] | T | object = _NO_DEFAULT
|
|
102
|
+
) -> SnakeCase[T]:
|
|
103
|
+
"""Builds a `CASE WHEN`. The branches are evaluated in order; the first match wins.
|
|
104
|
+
|
|
105
|
+
snake_case((User.age < 18, "minor"), (User.age < 65, "adult"), default="retired")
|
|
106
|
+
|
|
107
|
+
Without a `default` it returns NULL when none of them match (SQL semantics).
|
|
108
|
+
"""
|
|
109
|
+
if not branches:
|
|
110
|
+
raise SnakeEmitError(
|
|
111
|
+
"A CASE needs at least one branch (condition, value): with none there is no valid SQL "
|
|
112
|
+
"to emit, only a stray ELSE."
|
|
113
|
+
)
|
|
114
|
+
if default is _NO_DEFAULT:
|
|
115
|
+
return SnakeCase(branches=branches)
|
|
116
|
+
return SnakeCase(
|
|
117
|
+
branches=branches,
|
|
118
|
+
default=default, # type: ignore[arg-type]
|
|
119
|
+
has_default=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def snake_coalesce(first: SnakeValue[T], *rest: SnakeValue[T]) -> SnakeCoalesce[T]: ...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
def snake_coalesce(first: SnakeValue[T | None], fallback: T, /) -> SnakeCoalesce[T]: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@overload
|
|
132
|
+
def snake_coalesce(
|
|
133
|
+
first: SnakeValue[T], *rest: SnakeValue[T] | T
|
|
134
|
+
) -> SnakeCoalesce[T]: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def snake_coalesce(
|
|
138
|
+
first: SnakeValue[Any], *rest: SnakeValue[Any] | Any
|
|
139
|
+
) -> SnakeCoalesce[Any]:
|
|
140
|
+
"""Builds a `COALESCE(...)`: the first non-NULL argument.
|
|
141
|
+
|
|
142
|
+
The FIRST argument must be an expression: a literal there would never be NULL (it would always
|
|
143
|
+
return that literal) and it also anchors the type `T`.
|
|
144
|
+
|
|
145
|
+
THE MIDDLE OVERLOAD IS THE POINT: a `COALESCE` whose fallback is a LITERAL cannot be NULL, so it
|
|
146
|
+
drops the `| None`. `COALESCE(SUM(x), 0)` is an `int`, and that is the entire reason anybody
|
|
147
|
+
writes it — the value stops being nullable IN THE ENGINE, so it has to stop being nullable in the
|
|
148
|
+
type. Without it the declarator that exists to remove a `None` handed one back, and the caller
|
|
149
|
+
had to `cast()` in a project whose rule is zero `Any`.
|
|
150
|
+
|
|
151
|
+
The first overload is what keeps that from becoming a lie: with every argument an expression,
|
|
152
|
+
nothing guarantees a value and the nullability survives. Order matters — it has to be tried
|
|
153
|
+
before the literal one, or an expression fallback would be read as the literal.
|
|
154
|
+
"""
|
|
155
|
+
arguments: tuple[SnakeValue[Any] | Any, ...] = (first, *rest)
|
|
156
|
+
if len(arguments) < 2:
|
|
157
|
+
raise SnakeEmitError(
|
|
158
|
+
"COALESCE needs at least two arguments: with only one it chooses nothing, it is the "
|
|
159
|
+
"identity, and with zero there is nothing to choose from."
|
|
160
|
+
)
|
|
161
|
+
return SnakeCoalesce(arguments=arguments)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def snake_nullif(
|
|
165
|
+
value: SnakeValue[T], sentinel: SnakeValue[T] | T
|
|
166
|
+
) -> SnakeNullIf[T | None]:
|
|
167
|
+
"""Builds a `NULLIF(value, sentinel)`: turns a sentinel (the empty string) into NULL.
|
|
168
|
+
|
|
169
|
+
THE RESULT IS NULLABLE, and saying so is the whole point. This is the exact mirror of
|
|
170
|
+
`snake_coalesce`, which REMOVES a `None` and declares it: this one PUTS one in. It used to
|
|
171
|
+
return `SnakeNullIf[T]` — the same `T` it was given — so the declarator whose entire job is to
|
|
172
|
+
introduce a NULL was the one place the type did not mention it.
|
|
173
|
+
|
|
174
|
+
That is not a corner case. Guarding a division against zero is `x / snake_nullif(y, 0)`, and the
|
|
175
|
+
NULL it produces IS the guard working. Typing that `int` told the caller the result could not be
|
|
176
|
+
`None` in a project that asks people to trust the checker over the engine.
|
|
177
|
+
"""
|
|
178
|
+
return SnakeNullIf(value=value, sentinel=sentinel)
|