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.
Files changed (198) hide show
  1. snake_orm-0.1.0b1.dist-info/METADATA +366 -0
  2. snake_orm-0.1.0b1.dist-info/RECORD +198 -0
  3. snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
  4. snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
  5. snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
  6. snakeorm/__init__.py +214 -0
  7. snakeorm/advisor.py +109 -0
  8. snakeorm/cli/__init__.py +3 -0
  9. snakeorm/cli/app.py +992 -0
  10. snakeorm/cli/discovery.py +179 -0
  11. snakeorm/cli/hooks.py +86 -0
  12. snakeorm/compiler/__init__.py +3 -0
  13. snakeorm/compiler/compiler.py +330 -0
  14. snakeorm/connection.py +228 -0
  15. snakeorm/contrib/__init__.py +17 -0
  16. snakeorm/contrib/asgi.py +192 -0
  17. snakeorm/contrib/config.py +264 -0
  18. snakeorm/contrib/deliver.py +256 -0
  19. snakeorm/contrib/django.py +245 -0
  20. snakeorm/contrib/sidecar.py +44 -0
  21. snakeorm/contrib/wsgi.py +176 -0
  22. snakeorm/core/__init__.py +1 -0
  23. snakeorm/core/config.py +173 -0
  24. snakeorm/core/converters.py +172 -0
  25. snakeorm/core/exceptions.py +175 -0
  26. snakeorm/core/placement.py +15 -0
  27. snakeorm/core/sentinels.py +37 -0
  28. snakeorm/core/signals.py +114 -0
  29. snakeorm/debug/__init__.py +42 -0
  30. snakeorm/debug/assets/css/panel.css +616 -0
  31. snakeorm/debug/assets/css/snake_orm_app.css +293 -0
  32. snakeorm/debug/assets/img/icon.svg +5 -0
  33. snakeorm/debug/assets/js/history.js +425 -0
  34. snakeorm/debug/assets/js/language.js +283 -0
  35. snakeorm/debug/assets/js/panel.js +241 -0
  36. snakeorm/debug/assets/js/snake_orm_app.js +82 -0
  37. snakeorm/debug/assets/pages/cli.html +28 -0
  38. snakeorm/debug/assets/pages/config.html +34 -0
  39. snakeorm/debug/assets/pages/database_first.html +29 -0
  40. snakeorm/debug/assets/pages/help.html +21 -0
  41. snakeorm/debug/assets/pages/history.html +27 -0
  42. snakeorm/debug/capture.py +234 -0
  43. snakeorm/debug/channel.py +131 -0
  44. snakeorm/debug/collector.py +144 -0
  45. snakeorm/debug/config.py +149 -0
  46. snakeorm/debug/html.py +578 -0
  47. snakeorm/debug/matrix.py +60 -0
  48. snakeorm/debug/origin.py +42 -0
  49. snakeorm/debug/otel/__init__.py +68 -0
  50. snakeorm/debug/otel/context.py +43 -0
  51. snakeorm/debug/otel/exporter.py +277 -0
  52. snakeorm/debug/otel/payload.py +97 -0
  53. snakeorm/debug/otel/report.py +29 -0
  54. snakeorm/debug/otel/spans.py +393 -0
  55. snakeorm/debug/otel/summary.py +59 -0
  56. snakeorm/debug/record.py +54 -0
  57. snakeorm/debug/report.py +311 -0
  58. snakeorm/debug/testing.py +23 -0
  59. snakeorm/decorators/__init__.py +13 -0
  60. snakeorm/decorators/abstract.py +26 -0
  61. snakeorm/decorators/db_first.py +104 -0
  62. snakeorm/decorators/function.py +28 -0
  63. snakeorm/decorators/model.py +268 -0
  64. snakeorm/decorators/polymorphic.py +140 -0
  65. snakeorm/decorators/result.py +167 -0
  66. snakeorm/decorators/row.py +90 -0
  67. snakeorm/decorators/trigger.py +47 -0
  68. snakeorm/decorators/view.py +149 -0
  69. snakeorm/dialects/__init__.py +6 -0
  70. snakeorm/dialects/base.py +373 -0
  71. snakeorm/dialects/capabilities.py +571 -0
  72. snakeorm/dialects/literals.py +46 -0
  73. snakeorm/dialects/matrix.py +384 -0
  74. snakeorm/dialects/mysql.py +519 -0
  75. snakeorm/dialects/postgres.py +504 -0
  76. snakeorm/dialects/sqlite.py +417 -0
  77. snakeorm/drivers/__init__.py +18 -0
  78. snakeorm/drivers/asyncbase.py +73 -0
  79. snakeorm/drivers/asyncdecorators.py +244 -0
  80. snakeorm/drivers/asyncpool.py +228 -0
  81. snakeorm/drivers/asyncpsycopg.py +124 -0
  82. snakeorm/drivers/asyncpymysql.py +50 -0
  83. snakeorm/drivers/asyncsqlite.py +30 -0
  84. snakeorm/drivers/base.py +76 -0
  85. snakeorm/drivers/failures.py +181 -0
  86. snakeorm/drivers/logging.py +167 -0
  87. snakeorm/drivers/pool.py +352 -0
  88. snakeorm/drivers/psycopg.py +188 -0
  89. snakeorm/drivers/pymysql.py +176 -0
  90. snakeorm/drivers/savepoints.py +46 -0
  91. snakeorm/drivers/sqlite.py +170 -0
  92. snakeorm/drivers/threaded.py +143 -0
  93. snakeorm/drivers/timeout.py +87 -0
  94. snakeorm/dto/__init__.py +55 -0
  95. snakeorm/dto/read.py +231 -0
  96. snakeorm/dto/region.py +326 -0
  97. snakeorm/dto/resolve.py +354 -0
  98. snakeorm/dto/spec.py +201 -0
  99. snakeorm/expressions/__init__.py +88 -0
  100. snakeorm/expressions/conditional.py +178 -0
  101. snakeorm/expressions/expression.py +810 -0
  102. snakeorm/expressions/functions.py +71 -0
  103. snakeorm/expressions/keys.py +299 -0
  104. snakeorm/expressions/paths.py +43 -0
  105. snakeorm/expressions/scalar.py +293 -0
  106. snakeorm/expressions/window.py +248 -0
  107. snakeorm/fields/__init__.py +68 -0
  108. snakeorm/fields/check.py +79 -0
  109. snakeorm/fields/column.py +316 -0
  110. snakeorm/fields/enum.py +64 -0
  111. snakeorm/fields/index.py +46 -0
  112. snakeorm/fields/relationship.py +738 -0
  113. snakeorm/fields/typed.py +736 -0
  114. snakeorm/helpers/__init__.py +1 -0
  115. snakeorm/helpers/annotations.py +21 -0
  116. snakeorm/helpers/inheritance.py +41 -0
  117. snakeorm/helpers/pyliteral.py +52 -0
  118. snakeorm/helpers/pytype.py +50 -0
  119. snakeorm/introspection/__init__.py +13 -0
  120. snakeorm/introspection/base.py +30 -0
  121. snakeorm/introspection/drift.py +78 -0
  122. snakeorm/introspection/models.py +633 -0
  123. snakeorm/introspection/mysql.py +367 -0
  124. snakeorm/introspection/postgres.py +397 -0
  125. snakeorm/introspection/sqlite.py +217 -0
  126. snakeorm/introspection/unsupported.py +94 -0
  127. snakeorm/linker/__init__.py +3 -0
  128. snakeorm/linker/linker.py +521 -0
  129. snakeorm/metadata/__init__.py +41 -0
  130. snakeorm/metadata/check.py +37 -0
  131. snakeorm/metadata/column.py +201 -0
  132. snakeorm/metadata/enum_storage.py +33 -0
  133. snakeorm/metadata/fk_action.py +19 -0
  134. snakeorm/metadata/foreign_key.py +26 -0
  135. snakeorm/metadata/index.py +43 -0
  136. snakeorm/metadata/index_method.py +19 -0
  137. snakeorm/metadata/int_size.py +20 -0
  138. snakeorm/metadata/json_storage.py +21 -0
  139. snakeorm/metadata/polymorphic.py +30 -0
  140. snakeorm/metadata/primary_key.py +23 -0
  141. snakeorm/metadata/relationship.py +60 -0
  142. snakeorm/metadata/relationship_kind.py +43 -0
  143. snakeorm/metadata/routine.py +26 -0
  144. snakeorm/metadata/server_default.py +20 -0
  145. snakeorm/metadata/table.py +97 -0
  146. snakeorm/metadata/table_kind.py +23 -0
  147. snakeorm/metadata/trigger.py +62 -0
  148. snakeorm/metadata/type_params.py +348 -0
  149. snakeorm/migration/__init__.py +88 -0
  150. snakeorm/migration/asyncrunner.py +168 -0
  151. snakeorm/migration/autodetect.py +96 -0
  152. snakeorm/migration/ddl.py +1172 -0
  153. snakeorm/migration/diff.py +701 -0
  154. snakeorm/migration/loader.py +77 -0
  155. snakeorm/migration/operations.py +952 -0
  156. snakeorm/migration/planner.py +78 -0
  157. snakeorm/migration/realize.py +420 -0
  158. snakeorm/migration/renames.py +153 -0
  159. snakeorm/migration/render.py +1095 -0
  160. snakeorm/migration/runner.py +314 -0
  161. snakeorm/migration/squash.py +60 -0
  162. snakeorm/migration/state.py +107 -0
  163. snakeorm/model.py +162 -0
  164. snakeorm/py.typed +0 -0
  165. snakeorm/query/__init__.py +9 -0
  166. snakeorm/query/compound.py +288 -0
  167. snakeorm/query/join_kind.py +19 -0
  168. snakeorm/query/joined.py +143 -0
  169. snakeorm/query/query.py +1001 -0
  170. snakeorm/query/recursive.py +244 -0
  171. snakeorm/registry/__init__.py +5 -0
  172. snakeorm/registry/by_module.py +163 -0
  173. snakeorm/registry/registry.py +166 -0
  174. snakeorm/session/__init__.py +8 -0
  175. snakeorm/session/asyncsession.py +638 -0
  176. snakeorm/session/coercion.py +306 -0
  177. snakeorm/session/factory.py +29 -0
  178. snakeorm/session/guards.py +269 -0
  179. snakeorm/session/isolation.py +23 -0
  180. snakeorm/session/mapper.py +213 -0
  181. snakeorm/session/planning.py +815 -0
  182. snakeorm/session/retry.py +96 -0
  183. snakeorm/session/session.py +871 -0
  184. snakeorm/session/shared.py +355 -0
  185. snakeorm/sql/__init__.py +18 -0
  186. snakeorm/sql/adapt.py +104 -0
  187. snakeorm/sql/aggregate.py +232 -0
  188. snakeorm/sql/condition.py +359 -0
  189. snakeorm/sql/delete.py +46 -0
  190. snakeorm/sql/insert.py +149 -0
  191. snakeorm/sql/joins.py +158 -0
  192. snakeorm/sql/pk_subquery.py +54 -0
  193. snakeorm/sql/refs.py +19 -0
  194. snakeorm/sql/resolver.py +28 -0
  195. snakeorm/sql/select.py +175 -0
  196. snakeorm/sql/update.py +82 -0
  197. snakeorm/sql/value.py +552 -0
  198. snakeorm/times.py +276 -0
@@ -0,0 +1,97 @@
1
+ """Immutable metadata of a compiled model (the graph's root node)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from snakeorm.core.placement import DEFAULT_DATABASE, DEFAULT_SCHEMA
8
+ from snakeorm.metadata.check import SnakeCheckInfo
9
+ from snakeorm.metadata.column import SnakeColumnInfo
10
+ from snakeorm.metadata.index import SnakeIndexInfo
11
+ from snakeorm.metadata.polymorphic import SnakePolymorphicInfo
12
+ from snakeorm.metadata.primary_key import SnakePrimaryKeyInfo
13
+ from snakeorm.metadata.relationship import SnakeRelationshipInfo
14
+ from snakeorm.metadata.table_kind import SnakeTableKind
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class SnakeTableInfo:
19
+ """Compiled model: name, columns, primary key, relations and indexes.
20
+
21
+ This is the graph's root node. The whole ORM (SQL, migrations, query) reads from here;
22
+ the runtime never inspects the class again.
23
+ """
24
+
25
+ name: str
26
+ columns: tuple[SnakeColumnInfo, ...]
27
+ primary_key: SnakePrimaryKeyInfo
28
+ relationships: tuple[SnakeRelationshipInfo, ...] = ()
29
+ schema: str = DEFAULT_SCHEMA
30
+ # Connection the node lives on. An axis INDEPENDENT of `kind` (what it is vs where it lives).
31
+ database: str = DEFAULT_DATABASE
32
+ db_comment: str | None = None # COMMENT ON TABLE
33
+ indexes: tuple[SnakeIndexInfo, ...] = ()
34
+ # Domain rules the DB enforces (`CHECK (...)`). Condition as an AST, not emitted SQL.
35
+ checks: tuple[SnakeCheckInfo, ...] = ()
36
+ # What the node IS and who governs it, one single axis instead of booleans: see `SnakeTableKind`.
37
+ kind: SnakeTableKind = SnakeTableKind.TABLE
38
+ view_definition: str | None = None # the view's RAW SELECT (only with `sql=`)
39
+ view_query: object = None
40
+ """The `SnakeQuery` that defines the view, uncompiled (only with `query=`).
41
+
42
+ It is stored UNCOMPILED on purpose: a view's body is written differently on every engine
43
+ —quoting, schema qualification, literals, LIMIT/OFFSET— and compiling it at model declaration
44
+ time froze it into one engine's. The emitter is the one that compiles it, with the target
45
+ dialect.
46
+
47
+ Typed `object` and not `SnakeQuery` because `metadata/` sits BELOW `query/` in the layer graph:
48
+ annotating it with its real type would invert the dependency. Whoever uses it narrows it.
49
+ """
50
+ # EXPLICIT dependencies of this view on OTHER views (resolved names). Forces the migration's
51
+ # topological order (create A before B). Only between views; empty on a normal table.
52
+ depends_on: tuple[str, ...] = ()
53
+ # Participation in a POLYMORPHIC hierarchy. `None` on a normal table. Another axis independent
54
+ # of `kind`/`database`: see `SnakePolymorphicInfo`.
55
+ polymorphic: SnakePolymorphicInfo | None = None
56
+
57
+ @property
58
+ def is_polymorphic_child(self) -> bool:
59
+ """Whether it is a CHILD of a hierarchy: shares a table with the base, filtered by its value.
60
+
61
+ Since the physical table is a single one: autogen does NOT emit its `CREATE TABLE` (the base
62
+ does) and queries DO filter it by its discriminator.
63
+ """
64
+ return self.polymorphic is not None and not self.polymorphic.is_base
65
+
66
+ @property
67
+ def is_view(self) -> bool:
68
+ """Whether the node is a VIEW (read only, no constraints). Derived from `kind`."""
69
+ return self.kind is SnakeTableKind.VIEW
70
+
71
+ @property
72
+ def is_managed(self) -> bool:
73
+ """Whether MIGRATIONS govern this node.
74
+
75
+ An `EXTERNAL` mirror (`@snake_db_first`) is queried and written like any other model, but
76
+ autogen ignores it: its schema is governed by the DB, not by us.
77
+ """
78
+ return self.kind is not SnakeTableKind.EXTERNAL
79
+
80
+ def get_column(self, name: str) -> SnakeColumnInfo | None:
81
+ """Returns the column with that SQL name, or None if it does not exist."""
82
+ for column in self.columns:
83
+ if column.name == name:
84
+ return column
85
+ return None
86
+
87
+ def get_column_by_attr(self, attr: str) -> SnakeColumnInfo | None:
88
+ """Returns the column whose Python ATTRIBUTE is `attr`, or None if it does not exist.
89
+
90
+ With `snake_column(name=...)` the SQL name and the attribute's differ, and whoever walks the
91
+ graph uses the attribute. It tries `attr_name` and falls back to the SQL name (hand-written
92
+ metadata may not carry it).
93
+ """
94
+ for column in self.columns:
95
+ if column.attr_name == attr:
96
+ return column
97
+ return self.get_column(attr)
@@ -0,0 +1,23 @@
1
+ """What a graph node IS and WHO governs it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class SnakeTableKind(Enum):
9
+ """Nature of a graph node: one single axis, not a bag of booleans.
10
+
11
+ Replaces `is_view`/`managed`: N booleans give you illegal states (a managed external view?);
12
+ the enum makes them unwritable. `database` does NOT belong here (an axis independent of what
13
+ the node is).
14
+
15
+ - `TABLE`: a table we manage (create/alter/drop).
16
+ - `VIEW`: a view we manage (`@snake_view`). READ ONLY, no constraints.
17
+ - `EXTERNAL`: a mirror of something existing that we do NOT govern (`@snake_db_first`);
18
+ migrations IGNORE it.
19
+ """
20
+
21
+ TABLE = "table"
22
+ VIEW = "view"
23
+ EXTERNAL = "external"
@@ -0,0 +1,62 @@
1
+ """Metadata of a TRIGGER: the signal that lives in the database.
2
+
3
+ Against a code signal (`snakeorm/signals.py`), a trigger holds even when the row is written by
4
+ another process or by a `psql`, because the rule lives in the schema. That is why the `body` is SQL
5
+ and only SQL: no callables, this is the border between the two mechanisms.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+
13
+ from snakeorm.core.placement import DEFAULT_SCHEMA
14
+ from snakeorm.core.exceptions import SnakeModelDefinitionError
15
+
16
+
17
+ class SnakeTriggerTiming(Enum):
18
+ """When it fires relative to the operation. Engine-agnostic: all three are standard SQL."""
19
+
20
+ BEFORE = "BEFORE"
21
+ AFTER = "AFTER"
22
+ INSTEAD_OF = "INSTEAD OF"
23
+
24
+
25
+ class SnakeTriggerEvent(Enum):
26
+ """Which operation fires it."""
27
+
28
+ INSERT = "INSERT"
29
+ UPDATE = "UPDATE"
30
+ DELETE = "DELETE"
31
+ TRUNCATE = "TRUNCATE"
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class SnakeTriggerInfo:
36
+ """A trigger on a table: when, on what, and what it runs.
37
+
38
+ `body` is raw and opaque (`EXECUTE FUNCTION audit()`), as in `SnakeRoutineInfo`: the diff only
39
+ compares the string (typing PL/pgSQL would mean putting a whole language inside the ORM).
40
+ `for_each_row` tells a per-ROW trigger apart from a per-STATEMENT one; per row by default
41
+ (required for `NEW`/`OLD`).
42
+ """
43
+
44
+ name: str
45
+ table: str
46
+ timing: SnakeTriggerTiming
47
+ events: tuple[SnakeTriggerEvent, ...]
48
+ body: str
49
+ schema: str = DEFAULT_SCHEMA
50
+ for_each_row: bool = True
51
+
52
+ def __post_init__(self) -> None:
53
+ """A trigger with no events fires on nothing: a dead object.
54
+
55
+ It is rejected at DECLARATION time (not at emission), while the user still remembers what
56
+ they meant to write.
57
+ """
58
+ if not self.events:
59
+ raise SnakeModelDefinitionError(
60
+ f"Trigger '{self.name}' does not declare any event, so it would never fire. "
61
+ f"Declare at least one event (INSERT, UPDATE, DELETE or TRUNCATE)."
62
+ )
@@ -0,0 +1,348 @@
1
+ """SQL parameters of a column, grouped BY TYPE FAMILY.
2
+
3
+ Every Python type family has its own parameters: an `int` has width, a `str` has length, a
4
+ `Decimal` has precision and scale, a `dict` has backing. Loose and flat inside `SnakeColumnInfo`,
5
+ nothing stopped you from writing a column with `max_length` AND `precision` at once — a state no
6
+ engine can represent, living in the graph that is supposed to BE the truth.
7
+
8
+ Grouped, that combination cannot even be built. And the dialect now receives ONE parameter instead
9
+ of five: that is what finally lets `precision` enter `map_type` (it used to be concatenated onto
10
+ the type FROM OUTSIDE, with an f-string, which is why it was the only unvalidated one).
11
+
12
+ Adding a new family = a dataclass here and a branch in each dialect. Neither the compiler nor
13
+ `SnakeColumnInfo` gets touched: the guard is structural and reads `python_type`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import get_origin
19
+
20
+ from dataclasses import dataclass
21
+ from datetime import datetime, time
22
+ from decimal import Decimal
23
+
24
+ from snakeorm.core.exceptions import SnakeModelDefinitionError
25
+ from snakeorm.metadata.int_size import SnakeIntSize
26
+ from snakeorm.metadata.json_storage import SnakeJsonStorage
27
+
28
+
29
+ def base_type(python_type: object) -> object:
30
+ """A parameterised generic reduced to its ORIGIN; anything else unchanged.
31
+
32
+ `dict[str, object]` answers `dict`, `list[int]` answers `list`, and `bool` answers `bool` —
33
+ `get_origin` of a plain class is `None`, which is what keeps the identity comparison below
34
+ strict where it has to be.
35
+
36
+ It exists because the guard compared `python_type is self.python_type`, so a user who
37
+ parameterised their column —the only way to write a JSON column WITHOUT dragging in `Any`—
38
+ got a refusal at import, with a message that asked them to change `dict` into `dict`. The
39
+ project's rule number one is zero `Any`, and the ORM was forcing it.
40
+ """
41
+ return get_origin(python_type) or python_type
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class SnakeIntParams:
46
+ """Parameters of an `int` column: how much room it takes in the database."""
47
+
48
+ size: SnakeIntSize = SnakeIntSize.BIGINT
49
+
50
+ @property
51
+ def python_type(self) -> type:
52
+ """The Python type this family belongs to. The compiler's guard uses it."""
53
+ return int
54
+
55
+ def accepts(self, python_type: object) -> bool:
56
+ """The family covers EXACTLY its type. By identity, not by inheritance: `bool` is a
57
+ subclass of `int` and accepting it would size a boolean column as an integer.
58
+
59
+ The ORIGIN is what gets compared, so `dict[str, object]` is a `dict` — which is the only
60
+ way to declare a JSON column without a bare `dict` and the `Any` it drags in. It does not
61
+ loosen the identity: `get_origin(bool)` is `None`, so `bool` still compares as itself."""
62
+ return base_type(python_type) is self.python_type
63
+
64
+ @property
65
+ def declarator(self) -> str:
66
+ """The name of the field specifier that declares them. It goes into the error messages, so
67
+ the warning says WHAT to write and not just what is wrong."""
68
+ return "snake_int"
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class SnakeStrParams:
73
+ """Parameters of a `str` column: its maximum length, if it declares one.
74
+
75
+ The CEILING is set by each engine (Postgres and MySQL do not agree, and MySQL's even depends on
76
+ the collation); here we only head off what means nothing on any of them.
77
+ """
78
+
79
+ max_length: int | None = None
80
+ fixed: bool = False
81
+ """FIXED length (`CHAR(n)`) instead of variable (`VARCHAR(n)`).
82
+
83
+ It is not a stricter VARCHAR: `CHAR` pads with spaces up to `n` and compares ignoring that
84
+ padding. Whoever stores country codes, ISINs or a hash of known length wants it for exactly that
85
+ reason, and everybody else should not pay for it — which is why the default is still VARCHAR.
86
+ """
87
+
88
+ def __post_init__(self) -> None:
89
+ """A VARCHAR has to fit at least one character, and a CHAR has to say how many.
90
+
91
+ `VARCHAR(0)` is LEGAL on both engines and useless: it only accepts the empty string. That
92
+ the engine swallows it does not make it a reasonable declaration — nobody wants a column
93
+ like that — and this ORM shouts instead of letting through what is almost certainly a slip.
94
+ """
95
+ if self.max_length is not None and self.max_length < 1:
96
+ raise SnakeModelDefinitionError(
97
+ f"A VARCHAR has to fit at least one character, and max_length={self.max_length} "
98
+ f"was asked for. For unbounded text, do not declare max_length at all: "
99
+ f"snake_str() already emits TEXT."
100
+ )
101
+ if self.fixed and self.max_length is None:
102
+ # A bare `CHAR` is `CHAR(1)` in SQL. Guessing it would mean taking a decision the user
103
+ # never took on their behalf, and a one-character column where text was expected does
104
+ # not show up until something gets truncated.
105
+ raise SnakeModelDefinitionError(
106
+ "A fixed-length column has to say HOW MANY characters: declare "
107
+ "snake_str(max_length=n, fixed=True). A CHAR without a length is CHAR(1) in SQL, "
108
+ "and that is almost never what anyone wants."
109
+ )
110
+
111
+ @property
112
+ def python_type(self) -> type:
113
+ """The Python type this family belongs to. The compiler's guard uses it."""
114
+ return str
115
+
116
+ def accepts(self, python_type: object) -> bool:
117
+ """The family covers EXACTLY its type. By identity, not by inheritance: `bool` is a
118
+ subclass of `int` and accepting it would size a boolean column as an integer.
119
+
120
+ The ORIGIN is what gets compared, so `dict[str, object]` is a `dict` — which is the only
121
+ way to declare a JSON column without a bare `dict` and the `Any` it drags in. It does not
122
+ loosen the identity: `get_origin(bool)` is `None`, so `bool` still compares as itself."""
123
+ return base_type(python_type) is self.python_type
124
+
125
+ @property
126
+ def declarator(self) -> str:
127
+ """The name of the field specifier that declares them. It goes into the error messages, so
128
+ the warning says WHAT to write and not just what is wrong."""
129
+ return "snake_str"
130
+
131
+
132
+ @dataclass(frozen=True, slots=True)
133
+ class SnakeDecimalParams:
134
+ """Parameters of a `Decimal` column: the NUMERIC's digits.
135
+
136
+ `precision` is mandatory: a NUMERIC without it accepts any number of digits, so rounding stops
137
+ being declared and starts depending on whatever comes in.
138
+
139
+ Only what is absurd on ANY engine is rejected here. The ceiling —1000 digits in Postgres, 65 in
140
+ MySQL— is engine knowledge and lives in its dialect, like `max_bind_params`.
141
+
142
+ On scale: Postgres 15 accepts negative scales and scales greater than the precision, as its own
143
+ extension. It is not exposed, on purpose. The standard demands `0 <= scale <= precision`, MySQL
144
+ demands it, and Postgres demanded it up to 15; opening it would make a model stop being portable
145
+ depending on the VERSION of the server behind it, which is exactly what the Dialect/Driver axis
146
+ avoids.
147
+ """
148
+
149
+ precision: int
150
+ scale: int | None = None
151
+
152
+ def __post_init__(self) -> None:
153
+ """Heads off the three impossible numbers at construction, the earliest point there is."""
154
+ if self.precision < 1:
155
+ raise SnakeModelDefinitionError(
156
+ f"A NUMERIC has to have at least one digit, and "
157
+ f"precision={self.precision} was asked for. Precision is the TOTAL number of "
158
+ f"digits; the decimal places go in scale."
159
+ )
160
+ if self.scale is None:
161
+ return
162
+ if self.scale < 0:
163
+ raise SnakeModelDefinitionError(
164
+ f"The scale of a NUMERIC cannot be negative (scale={self.scale} was asked for). "
165
+ f"It is the decimal places: 2 for money, 0 for exact integers."
166
+ )
167
+ if self.scale > self.precision:
168
+ raise SnakeModelDefinitionError(
169
+ f"The scale cannot exceed the precision: {self.scale} decimal places were asked "
170
+ f"for inside {self.precision} total digits, leaving nothing for the integer "
171
+ f"part. Raise precision or lower scale."
172
+ )
173
+
174
+ @property
175
+ def python_type(self) -> type:
176
+ """The Python type this family belongs to. The compiler's guard uses it."""
177
+ return Decimal
178
+
179
+ def accepts(self, python_type: object) -> bool:
180
+ """The family covers EXACTLY its type. By identity, not by inheritance: `bool` is a
181
+ subclass of `int` and accepting it would size a boolean column as an integer.
182
+
183
+ The ORIGIN is what gets compared, so `dict[str, object]` is a `dict` — which is the only
184
+ way to declare a JSON column without a bare `dict` and the `Any` it drags in. It does not
185
+ loosen the identity: `get_origin(bool)` is `None`, so `bool` still compares as itself."""
186
+ return base_type(python_type) is self.python_type
187
+
188
+ @property
189
+ def declarator(self) -> str:
190
+ """The name of the field specifier that declares them. It goes into the error messages, so
191
+ the warning says WHAT to write and not just what is wrong."""
192
+ return "snake_decimal"
193
+
194
+
195
+ @dataclass(frozen=True, slots=True)
196
+ class SnakeJsonParams:
197
+ """Parameters of a `dict` column: which DB object backs it."""
198
+
199
+ storage: SnakeJsonStorage = SnakeJsonStorage.JSONB
200
+
201
+ @property
202
+ def python_type(self) -> type:
203
+ """The Python type this family belongs to. The compiler's guard uses it."""
204
+ return dict
205
+
206
+ def accepts(self, python_type: object) -> bool:
207
+ """The family covers EXACTLY its type. By identity, not by inheritance: `bool` is a
208
+ subclass of `int` and accepting it would size a boolean column as an integer.
209
+
210
+ The ORIGIN is what gets compared, so `dict[str, object]` is a `dict` — which is the only
211
+ way to declare a JSON column without a bare `dict` and the `Any` it drags in. It does not
212
+ loosen the identity: `get_origin(bool)` is `None`, so `bool` still compares as itself."""
213
+ return base_type(python_type) is self.python_type
214
+
215
+ @property
216
+ def declarator(self) -> str:
217
+ """The name of the field specifier that declares them. It goes into the error messages, so
218
+ the warning says WHAT to write and not just what is wrong."""
219
+ return "snake_json"
220
+
221
+
222
+ @dataclass(frozen=True, slots=True)
223
+ class SnakeDateTimeParams:
224
+ """Parameters of a date column: whether the column carries a zone, and its resolution.
225
+
226
+ `tz` is NOT a knob you pick by hand: the declarator sets it (`snake_datetimetz()` sets it to
227
+ `True`, `snake_datetime()` to `False`) and a compiler guard demands it match the annotation.
228
+ That way the MODEL says which column gets created —just as `snake_int(size=SMALLINT)` says
229
+ SMALLINT— without the redundancy being able to lie. Same treatment as `snake_enum(Status)` over
230
+ a `SnakeColumn[Status]`.
231
+
232
+ `precision` is the fractional-second digits: `0` whole seconds, `3` milliseconds, `6` the
233
+ Postgres default (exactly the resolution of Python's `datetime`).
234
+
235
+ Only what means nothing on ANY engine is rejected here —a negative digit count—. The CEILING is
236
+ set by each dialect: Postgres and MySQL stop at 6, SQL Server reaches 7 and Oracle 9, so pinning
237
+ it here would put a specific engine inside the model, and the project's golden rule is that the
238
+ metadata graph stays agnostic.
239
+ """
240
+
241
+ tz: bool = False
242
+ precision: int | None = None
243
+
244
+ def __post_init__(self) -> None:
245
+ """Heads off a negative precision at construction, the earliest point there is."""
246
+ if self.precision is not None and self.precision < 0:
247
+ raise SnakeModelDefinitionError(
248
+ f"The precision of a date cannot be negative ({self.precision} was asked for). "
249
+ f"It is the fractional-second digits: 0 is whole seconds, 3 milliseconds and "
250
+ f"6 microseconds, which is the resolution of Python's datetime."
251
+ )
252
+
253
+ @property
254
+ def python_type(self) -> type:
255
+ """The Python type this family belongs to. The compiler's guard uses it."""
256
+ return datetime
257
+
258
+ def accepts(self, python_type: object) -> bool:
259
+ """The family covers `datetime` AND `SnakeUtc`, which is a subclass of it.
260
+
261
+ The other families compare by identity; this one cannot, because `SnakeUtc` is the same data
262
+ type with a guarantee on top.
263
+ """
264
+ return isinstance(python_type, type) and issubclass(python_type, datetime)
265
+
266
+ @property
267
+ def declarator(self) -> str:
268
+ """The name of the field specifier that declares them. It goes into the error messages, so
269
+ the warning says WHAT to write and not just what is wrong."""
270
+ return "snake_datetime"
271
+
272
+
273
+ @dataclass(frozen=True, slots=True)
274
+ class SnakeFloatParams:
275
+ """Parameters of a `float` column: its WIDTH in bytes.
276
+
277
+ A Python `float` is double precision, so 8 is the default and it does not change: changing it
278
+ would make an already-written model lose precision silently on upgrade. Declaring 4 is a storage
279
+ decision —half the bytes per row— taken knowing what you lose.
280
+ """
281
+
282
+ size: int = 8
283
+
284
+ def __post_init__(self) -> None:
285
+ """Only 4 and 8: there is no engine with any other floating-point width.
286
+
287
+ Like every other structural guard, what means nothing on ANY engine dies at declaration
288
+ time. Per-engine ceilings are another matter and live in the dialect.
289
+ """
290
+ if self.size not in (4, 8):
291
+ raise SnakeModelDefinitionError(
292
+ f"A float is either 4 or 8 bytes, and size={self.size} was asked for. 4 is single "
293
+ f"precision (REAL/FLOAT) and 8 is double (DOUBLE PRECISION/DOUBLE), which is what a "
294
+ f"Python float is and the default of this ORM."
295
+ )
296
+
297
+ @property
298
+ def python_type(self) -> type:
299
+ """The Python type this family belongs to. The compiler's guard uses it."""
300
+ return float
301
+
302
+ def accepts(self, python_type: object) -> bool:
303
+ """The family covers EXACTLY its type, by identity and not by inheritance."""
304
+ return python_type is self.python_type
305
+
306
+ @property
307
+ def declarator(self) -> str:
308
+ """The name of the field specifier that declares them, so the error says WHAT to write."""
309
+ return "snake_float"
310
+
311
+
312
+ @dataclass(frozen=True, slots=True)
313
+ class SnakeTimeParams:
314
+ """Parameters of a `time` column: whether it carries a ZONE.
315
+
316
+ Two declarators, as with dates (`snake_datetime` / `snake_datetimetz`), and for the same reason:
317
+ the column SAYS which type it creates instead of it depending on whether the first value that
318
+ arrived carried an offset. A bare `TIME` throws the zone away, and an opening time stops meaning
319
+ the same thing when seen from somewhere else.
320
+ """
321
+
322
+ with_timezone: bool = False
323
+
324
+ @property
325
+ def python_type(self) -> type:
326
+ """The Python type this family belongs to. The compiler's guard uses it."""
327
+ return time
328
+
329
+ def accepts(self, python_type: object) -> bool:
330
+ """The family covers EXACTLY its type, by identity and not by inheritance."""
331
+ return python_type is self.python_type
332
+
333
+ @property
334
+ def declarator(self) -> str:
335
+ """The name of the field specifier that declares them, so the error says WHAT to write."""
336
+ return "snake_timetz" if self.with_timezone else "snake_time"
337
+
338
+
339
+ SnakeTypeParams = (
340
+ SnakeIntParams
341
+ | SnakeStrParams
342
+ | SnakeDecimalParams
343
+ | SnakeJsonParams
344
+ | SnakeDateTimeParams
345
+ | SnakeFloatParams
346
+ | SnakeTimeParams
347
+ )
348
+ """The parameters of ONE family. `None` on a column that has none (`bool`, `date`...)."""
@@ -0,0 +1,88 @@
1
+ """SnakeORM migrations: DDL generation from the compiled metadata."""
2
+
3
+ from snakeorm.migration.asyncrunner import AsyncMigrationRunner as AsyncMigrationRunner
4
+ from snakeorm.migration.autodetect import autodetect as autodetect
5
+ from snakeorm.migration.autodetect import current_routines as current_routines
6
+ from snakeorm.migration.autodetect import current_schema as current_schema
7
+ from snakeorm.migration.autodetect import replay as replay
8
+ from snakeorm.migration.ddl import emit_add_check as emit_add_check
9
+ from snakeorm.migration.ddl import emit_add_column as emit_add_column
10
+ from snakeorm.migration.ddl import emit_add_foreign_key as emit_add_foreign_key
11
+ from snakeorm.migration.ddl import emit_alter_column as emit_alter_column
12
+ from snakeorm.migration.ddl import emit_column_comment as emit_column_comment
13
+ from snakeorm.migration.ddl import emit_comments as emit_comments
14
+ from snakeorm.migration.ddl import emit_create_function as emit_create_function
15
+ from snakeorm.migration.ddl import emit_create_index as emit_create_index
16
+ from snakeorm.migration.ddl import emit_create_schema as emit_create_schema
17
+ from snakeorm.migration.ddl import emit_create_table as emit_create_table
18
+ from snakeorm.migration.ddl import emit_create_trigger as emit_create_trigger
19
+ from snakeorm.migration.ddl import emit_create_view as emit_create_view
20
+ from snakeorm.migration.ddl import emit_drop_check as emit_drop_check
21
+ from snakeorm.migration.ddl import emit_drop_column as emit_drop_column
22
+ from snakeorm.migration.ddl import emit_drop_foreign_key as emit_drop_foreign_key
23
+ from snakeorm.migration.ddl import foreign_key_name as foreign_key_name
24
+ from snakeorm.migration.ddl import emit_drop_function as emit_drop_function
25
+ from snakeorm.migration.ddl import emit_drop_index as emit_drop_index
26
+ from snakeorm.migration.ddl import emit_drop_schema as emit_drop_schema
27
+ from snakeorm.migration.ddl import emit_drop_table as emit_drop_table
28
+ from snakeorm.migration.ddl import emit_drop_trigger as emit_drop_trigger
29
+ from snakeorm.migration.ddl import emit_drop_view as emit_drop_view
30
+ from snakeorm.migration.ddl import emit_rebuild_table as emit_rebuild_table
31
+ from snakeorm.migration.ddl import emit_rename_column as emit_rename_column
32
+ from snakeorm.migration.ddl import emit_rename_table as emit_rename_table
33
+ from snakeorm.migration.ddl import emit_replace_view as emit_replace_view
34
+ from snakeorm.migration.ddl import emit_table_comment as emit_table_comment
35
+ from snakeorm.migration.ddl import inlined_foreign_keys as inlined_foreign_keys
36
+ from snakeorm.migration.ddl import sql_type_of as sql_type_of
37
+ from snakeorm.migration.diff import diff_routines as diff_routines
38
+ from snakeorm.migration.diff import diff_schema as diff_schema
39
+ from snakeorm.migration.diff import drop_order as drop_order
40
+ from snakeorm.migration.diff import diff_triggers as diff_triggers
41
+ from snakeorm.migration.loader import load as load
42
+ from snakeorm.migration.operations import AddCheck as AddCheck
43
+ from snakeorm.migration.operations import AddColumn as AddColumn
44
+ from snakeorm.migration.operations import AddForeignKey as AddForeignKey
45
+ from snakeorm.migration.operations import AlterColumn as AlterColumn
46
+ from snakeorm.migration.operations import AlterTableComment as AlterTableComment
47
+ from snakeorm.migration.operations import AlterFunction as AlterFunction
48
+ from snakeorm.migration.operations import AlterTrigger as AlterTrigger
49
+ from snakeorm.migration.operations import AlterView as AlterView
50
+ from snakeorm.migration.operations import CreateFunction as CreateFunction
51
+ from snakeorm.migration.operations import CreateIndex as CreateIndex
52
+ from snakeorm.migration.operations import CreateSchema as CreateSchema
53
+ from snakeorm.migration.operations import CreateTable as CreateTable
54
+ from snakeorm.migration.operations import CreateTrigger as CreateTrigger
55
+ from snakeorm.migration.operations import CreateView as CreateView
56
+ from snakeorm.migration.operations import DropCheck as DropCheck
57
+ from snakeorm.migration.operations import DropColumn as DropColumn
58
+ from snakeorm.migration.operations import DropForeignKey as DropForeignKey
59
+ from snakeorm.migration.operations import DropFunction as DropFunction
60
+ from snakeorm.migration.operations import DropIndex as DropIndex
61
+ from snakeorm.migration.operations import DropSchema as DropSchema
62
+ from snakeorm.migration.operations import DropTable as DropTable
63
+ from snakeorm.migration.operations import DropTrigger as DropTrigger
64
+ from snakeorm.migration.operations import DropView as DropView
65
+ from snakeorm.migration.operations import RebuildTable as RebuildTable
66
+ from snakeorm.migration.operations import RenameColumn as RenameColumn
67
+ from snakeorm.migration.operations import RenameTable as RenameTable
68
+ from snakeorm.migration.operations import RunPython as RunPython
69
+ from snakeorm.migration.operations import RunSQL as RunSQL
70
+ from snakeorm.migration.operations import SnakeDataOperation as SnakeDataOperation
71
+ from snakeorm.migration.operations import (
72
+ SnakeMigrationOperation as SnakeMigrationOperation,
73
+ )
74
+ from snakeorm.migration.operations import SnakeOperation as SnakeOperation
75
+ from snakeorm.migration.realize import realize as realize
76
+ from snakeorm.migration.renames import RenameSuggestion as RenameSuggestion
77
+ from snakeorm.migration.renames import format_narrowing_hint as format_narrowing_hint
78
+ from snakeorm.migration.renames import format_rename_hint as format_rename_hint
79
+ from snakeorm.migration.renames import narrowing_warnings as narrowing_warnings
80
+ from snakeorm.migration.renames import rename_suggestions as rename_suggestions
81
+ from snakeorm.migration.renames import standing_view_warning as standing_view_warning
82
+ from snakeorm.migration.render import render_migration as render_migration
83
+ from snakeorm.migration.runner import Migration as Migration
84
+ from snakeorm.migration.planner import dependency_order as dependency_order
85
+ from snakeorm.migration.runner import MigrationRunner as MigrationRunner
86
+ from snakeorm.migration.runner import squash_already_done as squash_already_done
87
+ from snakeorm.migration.squash import squash as squash
88
+ from snakeorm.migration.state import SchemaState as SchemaState