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,316 @@
|
|
|
1
|
+
"""The SnakeColumn descriptor and its field specifier snake_column."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Generic, Literal, TypeVar, overload
|
|
8
|
+
|
|
9
|
+
from snakeorm.core.exceptions import (
|
|
10
|
+
SnakeColumnNotLoaded,
|
|
11
|
+
SnakeModelDefinitionError,
|
|
12
|
+
)
|
|
13
|
+
from snakeorm.core.sentinels import NOT_LOADED
|
|
14
|
+
from snakeorm.expressions import SnakeExpr
|
|
15
|
+
from snakeorm.metadata import (
|
|
16
|
+
SnakeEnumStorage,
|
|
17
|
+
SnakeIntParams,
|
|
18
|
+
SnakeIntSize,
|
|
19
|
+
SnakeServerDefault,
|
|
20
|
+
SnakeTypeParams,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _MissingType:
|
|
27
|
+
"""Sentinel for 'no default value' (different from default=None)."""
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return "MISSING"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
MISSING = _MissingType()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SnakeColumn(Generic[T]):
|
|
37
|
+
"""Column descriptor.
|
|
38
|
+
|
|
39
|
+
CLASS access (`User.username`) → `SnakeExpr[T]` (an expression for queries).
|
|
40
|
+
INSTANCE access (`user.username`) → the stored value (`T`).
|
|
41
|
+
It also holds the SQL metadata the compiler will read to build the graph.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
primary_key: bool = False,
|
|
48
|
+
unique: bool = False,
|
|
49
|
+
default: object = MISSING,
|
|
50
|
+
default_factory: Callable[[], T] | None = None,
|
|
51
|
+
index: bool = False,
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
db_comment: str | None = None,
|
|
54
|
+
type_params: SnakeTypeParams | None = None,
|
|
55
|
+
declared_by: str | None = None,
|
|
56
|
+
autoincrement: bool = False,
|
|
57
|
+
is_discriminator: bool = False,
|
|
58
|
+
server_default: SnakeServerDefault | None = None,
|
|
59
|
+
server_default_sql: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
# `default` is a DDL literal; `default_factory` a Python callable. Mutually exclusive.
|
|
62
|
+
if default is not MISSING and default_factory is not None:
|
|
63
|
+
raise SnakeModelDefinitionError(
|
|
64
|
+
"Do not declare `default` and `default_factory` at the same time: `default` is the "
|
|
65
|
+
"DDL literal and `default_factory` is a Python callable. Pick one."
|
|
66
|
+
)
|
|
67
|
+
if default is not MISSING and callable(default):
|
|
68
|
+
raise SnakeModelDefinitionError(
|
|
69
|
+
"`default` must be a literal (it goes to the DDL as DEFAULT); got a callable. "
|
|
70
|
+
"For a value computed in Python (e.g. datetime.now) use `default_factory=`."
|
|
71
|
+
)
|
|
72
|
+
# `server_default` (an engine-agnostic enum) and `server_default_sql` (raw SQL): two
|
|
73
|
+
# shapes of the same concept (the DB supplies the value). Declaring both contradicts
|
|
74
|
+
# itself.
|
|
75
|
+
if server_default is not None and server_default_sql is not None:
|
|
76
|
+
raise SnakeModelDefinitionError(
|
|
77
|
+
"Do not declare `server_default` and `server_default_sql` at the same time: the enum is "
|
|
78
|
+
"the portable route and raw SQL its escape hatch. Pick one."
|
|
79
|
+
)
|
|
80
|
+
# `server_default` and `default` feed the same DDL `DEFAULT`: they cannot coexist.
|
|
81
|
+
if default is not MISSING and (
|
|
82
|
+
server_default is not None or server_default_sql is not None
|
|
83
|
+
):
|
|
84
|
+
raise SnakeModelDefinitionError(
|
|
85
|
+
"Do not declare `default` and `server_default`/`server_default_sql` at the same time: "
|
|
86
|
+
"both feed the DDL's DEFAULT. `default` is the client's literal; "
|
|
87
|
+
"`server_default` is the value the server supplies. Pick one."
|
|
88
|
+
)
|
|
89
|
+
self.primary_key = primary_key
|
|
90
|
+
self.unique = unique
|
|
91
|
+
self.default = default
|
|
92
|
+
self.default_factory = default_factory
|
|
93
|
+
self.index = index
|
|
94
|
+
self.db_comment = db_comment
|
|
95
|
+
# SQL parameters of the type FAMILY, or None if the type has none. The specifier of its
|
|
96
|
+
# family sets them (snake_int/snake_str/snake_decimal/snake_json); the compiler checks
|
|
97
|
+
# that the family matches the annotation.
|
|
98
|
+
self.type_params = type_params
|
|
99
|
+
self.declared_by = declared_by
|
|
100
|
+
"""The specifier the USER wrote, for the guards' messages.
|
|
101
|
+
|
|
102
|
+
It sits on the descriptor and NOT on `type_params`, which travels into the metadata
|
|
103
|
+
graph and is compared by equality — a field there would read as a phantom diff in
|
|
104
|
+
every migration. `None` means the family's own declarator is the right answer."""
|
|
105
|
+
self.autoincrement = (
|
|
106
|
+
autoincrement # the DB generates the value; optional in __init__
|
|
107
|
+
)
|
|
108
|
+
self.server_default = (
|
|
109
|
+
server_default # the DB supplies the value; excluded from __init__/INSERT
|
|
110
|
+
)
|
|
111
|
+
self.server_default_sql = (
|
|
112
|
+
server_default_sql # escape hatch: raw SQL, NOT portable
|
|
113
|
+
)
|
|
114
|
+
self._name_override = name
|
|
115
|
+
self._attr_name = ""
|
|
116
|
+
self._storage_key = ""
|
|
117
|
+
# `snake_enum` fills these in (None on a normal column). They live on the descriptor, not
|
|
118
|
+
# on a subtype, so that the compiler has ONE single walk over columns.
|
|
119
|
+
self.enum_type: type[Enum] | None = None
|
|
120
|
+
self.enum_storage: SnakeEnumStorage | None = None
|
|
121
|
+
# `snake_discriminator()` marks it: the column says which class each row is. Here, and not
|
|
122
|
+
# as a parameter of the decorator, so the hierarchy is declared in ONE place (the model).
|
|
123
|
+
self.is_discriminator = is_discriminator
|
|
124
|
+
# Filled in by the COMPILER, which is the only place that reads the annotation. Class access
|
|
125
|
+
# hands it to the expression so the SQL layer can reason about the type: the generic `T` is
|
|
126
|
+
# erased at runtime, and without it an integer division cannot be told from a decimal one.
|
|
127
|
+
# `None` until compiled, and every reader treats `None` as "no proof" and changes nothing.
|
|
128
|
+
self.python_type: type | None = None
|
|
129
|
+
|
|
130
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
131
|
+
"""Capture the property name and compute the per-instance storage key."""
|
|
132
|
+
self._attr_name = name
|
|
133
|
+
self._storage_key = f"__snake_{name}"
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def column_name(self) -> str:
|
|
137
|
+
"""SQL name of the column: the override if one was given, otherwise the property's."""
|
|
138
|
+
return self._name_override or self._attr_name
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def attr_name(self) -> str:
|
|
142
|
+
"""Python attribute name (for the SQL→Python trip back when mapping rows)."""
|
|
143
|
+
return self._attr_name
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def has_default(self) -> bool:
|
|
147
|
+
"""Whether the column has a default LITERAL (the one that goes to the DDL as DEFAULT)."""
|
|
148
|
+
return self.default is not MISSING
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def has_default_factory(self) -> bool:
|
|
152
|
+
"""Whether the column has a factory (a callable) that fills the value in Python."""
|
|
153
|
+
return self.default_factory is not None
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def has_server_default(self) -> bool:
|
|
157
|
+
"""Whether the DB supplies the value: the column is excluded from `__init__` and INSERT."""
|
|
158
|
+
return self.server_default is not None or self.server_default_sql is not None
|
|
159
|
+
|
|
160
|
+
@overload
|
|
161
|
+
def __get__(self, instance: None, owner: Any) -> SnakeExpr[T]: ...
|
|
162
|
+
@overload
|
|
163
|
+
def __get__(self, instance: object, owner: Any) -> T: ...
|
|
164
|
+
def __get__(self, instance: object | None, owner: Any) -> Any:
|
|
165
|
+
if instance is None:
|
|
166
|
+
return SnakeExpr(path=(self.column_name,), python_type=self.python_type)
|
|
167
|
+
value = getattr(instance, self._storage_key, self.default)
|
|
168
|
+
# THREE states, not two, and collapsing any pair of them is a bug of its own. A VALUE is
|
|
169
|
+
# returned; a column the query deliberately left out (`only()`/`defer()`) carries the
|
|
170
|
+
# sentinel and RAISES; an attribute simply never set on an instance nobody hydrated falls
|
|
171
|
+
# through to the default, which is what makes a hand-built model work.
|
|
172
|
+
#
|
|
173
|
+
# Without the middle one, a deferred column reads as its default: `None` for a name, `0` for
|
|
174
|
+
# a count. A wrong answer with no error is the one outcome this ORM does not produce.
|
|
175
|
+
if value is NOT_LOADED:
|
|
176
|
+
raise SnakeColumnNotLoaded(
|
|
177
|
+
f"Column '{self.column_name}' was not loaded: the query that built this row named "
|
|
178
|
+
f"only some columns (only()/defer()). Ask for it in the query, or use select() if "
|
|
179
|
+
f"what you want is the values rather than the model."
|
|
180
|
+
)
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
def __set__(self, instance: object, value: T) -> None:
|
|
184
|
+
object.__setattr__(instance, self._storage_key, value)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@overload
|
|
188
|
+
def snake_column(
|
|
189
|
+
*,
|
|
190
|
+
server_default: SnakeServerDefault,
|
|
191
|
+
server_default_sql: str | None = ...,
|
|
192
|
+
default: object = ...,
|
|
193
|
+
default_factory: Callable[[], Any] | None = ...,
|
|
194
|
+
primary_key: bool = ...,
|
|
195
|
+
unique: bool = ...,
|
|
196
|
+
index: bool = ...,
|
|
197
|
+
name: str | None = ...,
|
|
198
|
+
db_comment: str | None = ...,
|
|
199
|
+
init: Literal[False] = False,
|
|
200
|
+
) -> Any: ...
|
|
201
|
+
@overload
|
|
202
|
+
def snake_column(
|
|
203
|
+
*,
|
|
204
|
+
server_default_sql: str,
|
|
205
|
+
server_default: SnakeServerDefault | None = ...,
|
|
206
|
+
default: object = ...,
|
|
207
|
+
default_factory: Callable[[], Any] | None = ...,
|
|
208
|
+
primary_key: bool = ...,
|
|
209
|
+
unique: bool = ...,
|
|
210
|
+
index: bool = ...,
|
|
211
|
+
name: str | None = ...,
|
|
212
|
+
db_comment: str | None = ...,
|
|
213
|
+
init: Literal[False] = False,
|
|
214
|
+
) -> Any: ...
|
|
215
|
+
@overload
|
|
216
|
+
def snake_column(
|
|
217
|
+
*,
|
|
218
|
+
primary_key: bool = ...,
|
|
219
|
+
unique: bool = ...,
|
|
220
|
+
default: object = ...,
|
|
221
|
+
default_factory: Callable[[], Any] | None = ...,
|
|
222
|
+
index: bool = ...,
|
|
223
|
+
name: str | None = ...,
|
|
224
|
+
db_comment: str | None = ...,
|
|
225
|
+
) -> Any: ...
|
|
226
|
+
def snake_column(
|
|
227
|
+
*,
|
|
228
|
+
primary_key: bool = False,
|
|
229
|
+
unique: bool = False,
|
|
230
|
+
default: object = MISSING,
|
|
231
|
+
default_factory: Callable[[], Any] | None = None,
|
|
232
|
+
index: bool = False,
|
|
233
|
+
name: str | None = None,
|
|
234
|
+
db_comment: str | None = None,
|
|
235
|
+
server_default: SnakeServerDefault | None = None,
|
|
236
|
+
server_default_sql: str | None = None,
|
|
237
|
+
init: Literal[False] = False,
|
|
238
|
+
) -> Any:
|
|
239
|
+
"""Declare a column WITHOUT type parameters: `bool`, `date`, `UUID`, `bytes`, `timedelta`...
|
|
240
|
+
|
|
241
|
+
The type comes from the annotation; this only adds type-agnostic SQL metadata.
|
|
242
|
+
|
|
243
|
+
There are NO type-specific knobs. `int_size`, `max_length`, `json_storage` and
|
|
244
|
+
`precision`/`scale` each live in the specifier of THEIR family (`snake_int`, `snake_str`,
|
|
245
|
+
`snake_json`, `snake_decimal`): offering them here made them autocomplete on EVERY column, and
|
|
246
|
+
a `max_length` on an integer is an illegal state that could be written. The type rules.
|
|
247
|
+
|
|
248
|
+
There is NO `nullable`: nullability is stated by the annotation alone
|
|
249
|
+
(`SnakeColumn[str | None]`); two sources would allow a type that lies.
|
|
250
|
+
|
|
251
|
+
`name` renames the SQL column. `default` is a DDL literal; `default_factory` a Python callable
|
|
252
|
+
that never touches the DDL. `server_default`/`server_default_sql` (a portable enum or raw SQL)
|
|
253
|
+
declare a SERVER value: the column is excluded from `__init__` and INSERT (RETURNING brings it
|
|
254
|
+
back). All the default sources are mutually exclusive.
|
|
255
|
+
|
|
256
|
+
`init` is not passed by hand: it is the `Literal[False]` signal that excludes from the
|
|
257
|
+
constructor the columns with `server_default` (through the overloads), just like `snake_auto`.
|
|
258
|
+
"""
|
|
259
|
+
del init # typing signal only; the runtime excludes via `server_default`
|
|
260
|
+
return SnakeColumn(
|
|
261
|
+
primary_key=primary_key,
|
|
262
|
+
unique=unique,
|
|
263
|
+
default=default,
|
|
264
|
+
default_factory=default_factory,
|
|
265
|
+
index=index,
|
|
266
|
+
name=name,
|
|
267
|
+
db_comment=db_comment,
|
|
268
|
+
server_default=server_default,
|
|
269
|
+
server_default_sql=server_default_sql,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def snake_auto(
|
|
274
|
+
*,
|
|
275
|
+
name: str | None = None,
|
|
276
|
+
db_comment: str | None = None,
|
|
277
|
+
int_size: SnakeIntSize = SnakeIntSize.BIGINT,
|
|
278
|
+
init: Literal[False] = False,
|
|
279
|
+
) -> Any:
|
|
280
|
+
"""Declare an autoincrementing PK: the DB generates the value.
|
|
281
|
+
|
|
282
|
+
It is excluded from the constructor (the `init: Literal[False]` is the typing signal): the id
|
|
283
|
+
shows up after the INSERT (RETURNING). For an explicit id, assign it as an attribute.
|
|
284
|
+
|
|
285
|
+
`int_size` fixes the width of the PK (default `BIGINT` → `BIGSERIAL`); lower it to `INTEGER` on
|
|
286
|
+
small catalogues.
|
|
287
|
+
"""
|
|
288
|
+
return SnakeColumn(
|
|
289
|
+
primary_key=True,
|
|
290
|
+
autoincrement=True,
|
|
291
|
+
name=name,
|
|
292
|
+
db_comment=db_comment,
|
|
293
|
+
type_params=SnakeIntParams(size=int_size),
|
|
294
|
+
declared_by="snake_auto",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def snake_discriminator(
|
|
299
|
+
*,
|
|
300
|
+
name: str | None = None,
|
|
301
|
+
index: bool = True,
|
|
302
|
+
db_comment: str | None = None,
|
|
303
|
+
init: Literal[False] = False,
|
|
304
|
+
) -> Any:
|
|
305
|
+
"""Declare the column that says WHICH CLASS each row is in a polymorphic hierarchy.
|
|
306
|
+
|
|
307
|
+
Here, and not as a parameter of the decorator: the `init: Literal[False]` excludes it from
|
|
308
|
+
`__init__` (the runtime fills it in by itself). Its value comes from the CLASS: each subclass
|
|
309
|
+
its own in `@snake_model(discriminator_value=...)`, the base its name in lowercase.
|
|
310
|
+
|
|
311
|
+
`index=True` by default because every query on a subclass carries
|
|
312
|
+
`WHERE <discriminator> = ...`: without an index the whole hierarchy gets scanned on each read.
|
|
313
|
+
"""
|
|
314
|
+
return SnakeColumn(
|
|
315
|
+
name=name, index=index, db_comment=db_comment, is_discriminator=True
|
|
316
|
+
)
|
snakeorm/fields/enum.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Declaring enumeration columns: `snake_enum(...)`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
from snakeorm.core.exceptions import SnakeModelDefinitionError
|
|
9
|
+
from snakeorm.fields.column import MISSING, SnakeColumn
|
|
10
|
+
from snakeorm.metadata import SnakeEnumStorage
|
|
11
|
+
|
|
12
|
+
E = TypeVar("E", bound=Enum)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def snake_enum(
|
|
16
|
+
enum_type: type[E],
|
|
17
|
+
*,
|
|
18
|
+
storage: SnakeEnumStorage = SnakeEnumStorage.CHECK,
|
|
19
|
+
default: E | object = MISSING,
|
|
20
|
+
primary_key: bool = False,
|
|
21
|
+
unique: bool = False,
|
|
22
|
+
index: bool = False,
|
|
23
|
+
name: str | None = None,
|
|
24
|
+
db_comment: str | None = None,
|
|
25
|
+
) -> Any:
|
|
26
|
+
"""Declare a column whose value is a member of a `StrEnum` or an `IntEnum`.
|
|
27
|
+
|
|
28
|
+
status: SnakeColumn[Status] = snake_enum(Status, default=Status.ACTIVE)
|
|
29
|
+
reason: SnakeColumn[Status | None] = snake_enum(Status) # nullable by the annotation
|
|
30
|
+
|
|
31
|
+
`default=Status.ACTIVE` must be a MEMBER of the enum: passing `"active"` or `42` is rejected by
|
|
32
|
+
a runtime guard (the `| object` in the signature stops the checker from catching it; without
|
|
33
|
+
the guard, an invalid default reached the DDL silently).
|
|
34
|
+
|
|
35
|
+
No `nullable` (the annotation states nullability). `storage` picks which DB object backs the
|
|
36
|
+
rule (see `SnakeEnumStorage`).
|
|
37
|
+
"""
|
|
38
|
+
if not (isinstance(enum_type, type) and issubclass(enum_type, Enum)):
|
|
39
|
+
raise SnakeModelDefinitionError(
|
|
40
|
+
f"snake_enum expects an enum class; got {enum_type!r}."
|
|
41
|
+
)
|
|
42
|
+
if not issubclass(enum_type, (int, str)):
|
|
43
|
+
raise SnakeModelDefinitionError(
|
|
44
|
+
f"{enum_type.__name__} has to be a StrEnum or an IntEnum: those are the ones that bring a "
|
|
45
|
+
f"base type (str or int) to store them with. A bare Enum with arbitrary values has no "
|
|
46
|
+
f"obvious representation in SQL, and guessing one would be worse than refusing."
|
|
47
|
+
)
|
|
48
|
+
if default is not MISSING and not isinstance(default, enum_type):
|
|
49
|
+
raise SnakeModelDefinitionError(
|
|
50
|
+
f"default={default!r} is not a member of {enum_type.__name__}. Pass a MEMBER of the "
|
|
51
|
+
f"enum (e.g. {enum_type.__name__}.{next(iter(enum_type)).name}), not its raw value: a "
|
|
52
|
+
f"default that does not belong to the enum would end up as a loose literal in the DDL's DEFAULT."
|
|
53
|
+
)
|
|
54
|
+
column: SnakeColumn[Any] = SnakeColumn(
|
|
55
|
+
primary_key=primary_key,
|
|
56
|
+
unique=unique,
|
|
57
|
+
default=default,
|
|
58
|
+
index=index,
|
|
59
|
+
name=name,
|
|
60
|
+
db_comment=db_comment,
|
|
61
|
+
)
|
|
62
|
+
column.enum_type = enum_type
|
|
63
|
+
column.enum_storage = storage
|
|
64
|
+
return column
|
snakeorm/fields/index.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Declaring indexes: SnakeIndex (used in the body of the model)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from snakeorm.expressions import SnakeCondition, SnakeExpr
|
|
8
|
+
from snakeorm.fields.column import SnakeColumn
|
|
9
|
+
from snakeorm.metadata import SnakeIndexMethod
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SnakeIndex:
|
|
13
|
+
"""Declare an index referencing the model's LOCAL columns (typed, no strings).
|
|
14
|
+
|
|
15
|
+
It holds references to the descriptors; the names get resolved at compile time, once
|
|
16
|
+
`__set_name__` has run. `where` (a PARTIAL index) and `method` are also declared here.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*columns: SnakeColumn[Any] | SnakeExpr[Any],
|
|
22
|
+
unique: bool = False,
|
|
23
|
+
name: str | None = None,
|
|
24
|
+
where: SnakeCondition | None = None,
|
|
25
|
+
method: SnakeIndexMethod | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._columns = columns
|
|
28
|
+
self.unique = unique
|
|
29
|
+
self.name = name
|
|
30
|
+
# PARTIAL index: it only indexes the rows that satisfy the condition. With `unique=True`
|
|
31
|
+
# it comes out as a unique index, not a constraint (Postgres has no `UNIQUE ... WHERE`).
|
|
32
|
+
self.where = where
|
|
33
|
+
self.method = method
|
|
34
|
+
|
|
35
|
+
def column_names(self) -> tuple[str, ...]:
|
|
36
|
+
"""SQL names of the index columns, whether they come as a DESCRIPTOR or as an EXPRESSION.
|
|
37
|
+
|
|
38
|
+
Both cases because of the project's dual behaviour: inside the body, `name` is the raw
|
|
39
|
+
descriptor (its name resolved at compile time); outside, `Customer.name` is CLASS access →
|
|
40
|
+
`SnakeExpr`. Accepting only the descriptor left `SnakeIndex` useless outside the body,
|
|
41
|
+
which is exactly where a PARTIAL index gets declared.
|
|
42
|
+
"""
|
|
43
|
+
return tuple(
|
|
44
|
+
column.path[-1] if isinstance(column, SnakeExpr) else column.column_name
|
|
45
|
+
for column in self._columns
|
|
46
|
+
)
|