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,47 @@
1
+ """Declaring TRIGGERS: the signal that lives inside the database.
2
+
3
+ It mirrors `snake_function`: you declare it, it gets registered and the autodetect compares it
4
+ against the history (source of truth: the registry). It does NOT accept a Python callable on
5
+ purpose: the trigger holds even if another process or a `psql` does the writing, because the rule
6
+ lives in the engine. For Python code there is `snake_on`, which only fires if the write goes
7
+ through the session.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Sequence
13
+
14
+ from snakeorm.core.placement import DEFAULT_SCHEMA
15
+ from snakeorm.metadata import SnakeTriggerEvent, SnakeTriggerInfo, SnakeTriggerTiming
16
+ from snakeorm.registry import SnakeRegistry
17
+ from snakeorm.registry import registry as default_registry
18
+
19
+
20
+ def snake_trigger(
21
+ *,
22
+ name: str,
23
+ table: str,
24
+ timing: SnakeTriggerTiming,
25
+ events: Sequence[SnakeTriggerEvent],
26
+ body: str,
27
+ schema: str = DEFAULT_SCHEMA,
28
+ for_each_row: bool = True,
29
+ registry: SnakeRegistry = default_registry,
30
+ ) -> SnakeTriggerInfo:
31
+ """Declare and register a desired trigger; return its `SnakeTriggerInfo`.
32
+
33
+ `body` is raw, opaque SQL: the diff only compares it as a string. Redeclaring the same
34
+ `(table, name)` replaces the previous one; the key carries the table because in Postgres the
35
+ name of a trigger is not unique on its own.
36
+ """
37
+ trigger = SnakeTriggerInfo(
38
+ name=name,
39
+ table=table,
40
+ timing=timing,
41
+ events=tuple(events),
42
+ body=body,
43
+ schema=schema,
44
+ for_each_row=for_each_row,
45
+ )
46
+ registry.register_trigger(trigger)
47
+ return trigger
@@ -0,0 +1,149 @@
1
+ """The @snake_view decorator: it maps a database VIEW as a READ-ONLY model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import replace
7
+ from typing import Any, TypeAlias, TypeVar, dataclass_transform
8
+
9
+ from snakeorm.compiler import compile_model
10
+ from snakeorm.decorators.model import _install_dunders, _make_init
11
+ from snakeorm.core.placement import DEFAULT_SCHEMA
12
+ from snakeorm.core.exceptions import SnakeModelDefinitionError
13
+ from snakeorm.fields import (
14
+ snake_auto,
15
+ snake_column,
16
+ snake_datetime,
17
+ snake_datetimetz,
18
+ snake_float,
19
+ snake_time,
20
+ snake_timetz,
21
+ snake_decimal,
22
+ snake_discriminator,
23
+ snake_enum,
24
+ snake_int,
25
+ snake_json,
26
+ snake_str,
27
+ snake_to_many,
28
+ snake_to_many_through,
29
+ snake_to_one,
30
+ )
31
+ from snakeorm.metadata import SnakeTableKind
32
+ from snakeorm.query import SnakeCompound, SnakeQuery
33
+ from snakeorm.registry import SnakeRegistry
34
+ from snakeorm.registry import registry as default_registry
35
+
36
+ T = TypeVar("T")
37
+
38
+ SnakeViewBody: TypeAlias = "SnakeQuery[Any] | SnakeCompound[Any]"
39
+ """What may DEFINE a view: one SELECT, or a set operation over several.
40
+
41
+ A compound was always executed correctly here —`view_body` only asks the object for `to_sql`, and
42
+ it was measured creating and querying the view on the three engines— and the annotation still said
43
+ `SnakeQuery`, which sent the caller to a `# type: ignore` over something the ORM does properly. A
44
+ view over a `UNION` is half the views there are.
45
+
46
+ `SnakeRecursive` is deliberately NOT here: a `WITH` inside a `CREATE VIEW` is its own question per
47
+ engine, and it has not been measured. What is declared is what has been.
48
+ """
49
+
50
+
51
+ def _view_source(sql: str | None, query: SnakeViewBody | None) -> None:
52
+ """Demand EXACTLY one source for the view definition (both or neither is an error)."""
53
+ if (sql is None) == (query is None):
54
+ raise SnakeModelDefinitionError(
55
+ "@snake_view requires EXACTLY one source for the definition: `sql=` (a raw SELECT) "
56
+ "or `query=` (a SnakeQuery, or a UNION/EXCEPT/INTERSECT of them) — never both, and "
57
+ "never neither."
58
+ )
59
+
60
+
61
+ def _dependency_name(dep: type, reg: SnakeRegistry) -> str:
62
+ """Resolve a `depends_on` class to the NAME of its view (we store names, not classes).
63
+
64
+ It only admits OTHER `@snake_view` views (already registered). If it is not registered or is
65
+ not a view, that is an error: a view does not depend on a table through `depends_on` (tables
66
+ are always created first).
67
+ """
68
+ # It is looked up in the SAME registry where the view is declared: an isolated view of that
69
+ # same store would not be found in the global one, and the error would say "it is not a view"
70
+ # when it very much is.
71
+ table = reg.table_of(dep)
72
+ if table is None or not table.is_view:
73
+ raise SnakeModelDefinitionError(
74
+ f"depends_on on @snake_view only admits OTHER @snake_view views; "
75
+ f"{getattr(dep, '__name__', dep)!r} is not a registered view. A view is always created "
76
+ f"after ALL the tables, so tables are never declared in depends_on."
77
+ )
78
+ return table.name
79
+
80
+
81
+ # A LITERAL tuple as PEP 681 demands; tied to SNAKE_FIELD_SPECIFIERS by a test.
82
+ @dataclass_transform(
83
+ kw_only_default=True,
84
+ field_specifiers=(
85
+ snake_column,
86
+ snake_auto,
87
+ snake_enum,
88
+ snake_int,
89
+ snake_str,
90
+ snake_decimal,
91
+ snake_datetime,
92
+ snake_datetimetz,
93
+ snake_float,
94
+ snake_time,
95
+ snake_timetz,
96
+ snake_json,
97
+ snake_to_one,
98
+ snake_to_many,
99
+ snake_to_many_through,
100
+ snake_discriminator,
101
+ ),
102
+ )
103
+ def snake_view(
104
+ *,
105
+ sql: str | None = None,
106
+ query: SnakeViewBody | None = None,
107
+ name: str | None = None,
108
+ schema: str = DEFAULT_SCHEMA,
109
+ depends_on: Sequence[type] = (),
110
+ registry: SnakeRegistry = default_registry,
111
+ ) -> Callable[[type[T]], type[T]]:
112
+ """Compile a `SnakeView` class as a READ-ONLY database VIEW.
113
+
114
+ The same TYPED columns as a model, but the node is marked `kind=SnakeTableKind.VIEW` and it
115
+ holds the SELECT (`view_definition`). Creating/editing/dropping it lives in the migrations, not
116
+ in the session. Relation navigation works in both directions as pure SQL generation: the DB
117
+ does not guarantee the FK of a view.
118
+
119
+ `depends_on` lists the OTHER `@snake_view` views that THIS one reads: the migration creates it
120
+ AFTER them (topological order) and drops it BEFORE. Only between views (a view gets created
121
+ after ALL the tables).
122
+
123
+ `registry` lets you declare it in an ISOLATED store, just like `@snake_model` and
124
+ `@snake_db_first`.
125
+ """
126
+ _view_source(sql, query)
127
+
128
+ def wrap(klass: type[T]) -> type[T]:
129
+ dependencies = tuple(_dependency_name(dep, registry) for dep in depends_on)
130
+ table = compile_model(
131
+ klass, table=name, schema=schema, kind=SnakeTableKind.VIEW
132
+ )
133
+ registry.register(
134
+ klass,
135
+ replace(
136
+ table,
137
+ # The query is stored UNCOMPILED: the body of a view is written differently on
138
+ # each engine, and compiling it here froze it into one of them. The emitter
139
+ # compiles it.
140
+ view_definition=sql,
141
+ view_query=query,
142
+ depends_on=dependencies,
143
+ ),
144
+ )
145
+ setattr(klass, "__init__", _make_init(klass))
146
+ _install_dunders(klass, registry)
147
+ return klass
148
+
149
+ return wrap
@@ -0,0 +1,6 @@
1
+ """SnakeORM's SQL dialects: how the SQL is written for each engine."""
2
+
3
+ from snakeorm.dialects.base import SnakeDialect as SnakeDialect
4
+ from snakeorm.dialects.mysql import MySQLDialect as MySQLDialect
5
+ from snakeorm.dialects.postgres import PostgresDialect as PostgresDialect
6
+ from snakeorm.dialects.sqlite import SQLiteDialect as SQLiteDialect
@@ -0,0 +1,373 @@
1
+ """The Protocol that defines a SQL dialect (how the SQL is WRITTEN)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Protocol, runtime_checkable
7
+
8
+ from snakeorm.dialects.capabilities import (
9
+ SnakeCapabilities,
10
+ SnakeLimits,
11
+ SnakeSyntax,
12
+ )
13
+ from snakeorm.expressions.scalar import SnakeFunc
14
+ from snakeorm.metadata import (
15
+ SnakeIndexMethod,
16
+ SnakeServerDefault,
17
+ SnakeTypeParams,
18
+ )
19
+
20
+
21
+ @runtime_checkable
22
+ class SnakeDialect(Protocol):
23
+ """How the SQL is written for one specific engine. It executes NOTHING.
24
+
25
+ It only enters `sql/` (emission). The graph and the models are 100% engine-agnostic.
26
+ Adding a new engine = one implementation of this Protocol, without touching the core.
27
+ """
28
+
29
+ capabilities: SnakeCapabilities
30
+ """What the engine KNOWS how to do, answered against the WHOLE `Cap` catalogue. The source of truth.
31
+
32
+ It is one object and not twenty loose attributes for two reasons the attributes could not give:
33
+ it can be WALKED (that is where the startup warning comes from, one for each thing the engine
34
+ does not give) and it can say "halfway" (SQLite stores an exact `Decimal` and sorts it as text:
35
+ neither absent nor full).
36
+ """
37
+ syntax: SnakeSyntax
38
+ """Differences in the SHAPE of the statement. They are translated in the emitter; they never stop the plan."""
39
+ limits: SnakeLimits
40
+ """The engine's numeric ceilings. `None` is not "no ceiling": it is "it ignores the declared parameter"."""
41
+
42
+ # The same old `supports_*`, `triggers_are_table_scoped` and `max_*` still exist as READ-ONLY
43
+ # properties derived from the three objects above, so the reads already written all over the
44
+ # package keep working while they migrate. `DerivedFlags` implements them. Declared as
45
+ # properties and not as attributes on purpose: a plain attribute DOES satisfy a read-only
46
+ # property in a Protocol, so a dialect can use either of the two forms; the other way round it
47
+ # cannot.
48
+
49
+ @property
50
+ def supports_returning(self) -> bool:
51
+ """Whether it can return the rows it wrote (`INSERT ... RETURNING`)."""
52
+ ...
53
+
54
+ @property
55
+ def supports_row_constructor(self) -> bool:
56
+ """Whether it understands `(a, b) IN ((...), (...))`. If not, the emitter uses the equivalent OR-of-ANDs."""
57
+ ...
58
+
59
+ @property
60
+ def supports_transactional_ddl(self) -> bool:
61
+ """Whether DDL goes inside the transaction. With it, an N-step migration is all-or-nothing."""
62
+ ...
63
+
64
+ @property
65
+ def supports_upsert(self) -> bool:
66
+ """Whether it can do an INSERT that is idempotent on conflict. Without it, `session.upsert()` raises (it does not emulate: there is a race)."""
67
+ ...
68
+
69
+ @property
70
+ def supports_add_constraint(self) -> bool:
71
+ """Whether it accepts `ALTER TABLE ... ADD CONSTRAINT`. It decides the SHAPE of the plan: with
72
+ it the FKs go AT THE END with no ordering between tables; without it (SQLite) they go INSIDE the CREATE TABLE and force a topological order."""
73
+ ...
74
+
75
+ @property
76
+ def supports_alter_column(self) -> bool:
77
+ """Whether it can change a column's type/nullability/default. SQLite cannot: it would require rebuilding the table."""
78
+ ...
79
+
80
+ @property
81
+ def supports_schemas(self) -> bool:
82
+ """Whether it has named schemas (`CREATE SCHEMA`). On SQLite the "schemas" are ATTACHed databases (`ATTACH`)."""
83
+ ...
84
+
85
+ @property
86
+ def supports_stored_functions(self) -> bool:
87
+ """Whether it stores named functions that a migration can create.
88
+
89
+ A capability of its OWN ever since the catalogue exists. The plan used to ask about
90
+ `supports_schemas` to decide whether it could create a function: it matched on all three
91
+ engines, so it worked and nobody saw it, but they are different things and a new engine
92
+ would have inherited the confusion.
93
+ """
94
+ ...
95
+
96
+ @property
97
+ def supports_row_locking(self) -> bool:
98
+ """Whether it can lock ROWS (`SELECT ... FOR UPDATE`). SQLite locks the file: asking for it fails at compile time."""
99
+ ...
100
+
101
+ @property
102
+ def supports_comments(self) -> bool:
103
+ """Whether it STORES table and column comments. Only SQLite does not.
104
+
105
+ Not "whether it has `COMMENT ON`": MySQL has no such statement and stores comments all the
106
+ same, as a clause. The spelling lives in `syntax.comment_style`.
107
+ """
108
+ ...
109
+
110
+ @property
111
+ def supports_replace_view(self) -> bool:
112
+ """Whether it can do `CREATE OR REPLACE VIEW`. Without it, altering a view is drop+create, and `realize()` rewrites it."""
113
+ ...
114
+
115
+ @property
116
+ def supports_parenthesised_compound(self) -> bool:
117
+ """Whether the branches of a UNION/EXCEPT/INTERSECT may go in PARENTHESES. It is not cosmetic:
118
+ the parentheses make a `LIMIT` belong to the branch and not to the whole set. SQLite rejects them."""
119
+ ...
120
+
121
+ @property
122
+ def supports_cte_in_compound_branch(self) -> bool:
123
+ """Whether a `WITH [RECURSIVE] ...` may be a BRANCH of a UNION/EXCEPT/INTERSECT.
124
+
125
+ A different question from `supports_parenthesised_compound`, and the two only look alike from
126
+ Postgres: MySQL parenthesises branches and still refuses a CTE inside one."""
127
+ ...
128
+
129
+ @property
130
+ def supports_ilike(self) -> bool:
131
+ """Whether it has `ILIKE`. Without it (SQLite) the emission falls back to `LOWER(a) LIKE LOWER(b)`, with ASCII-only folding."""
132
+ ...
133
+
134
+ @property
135
+ def triggers_are_table_scoped(self) -> bool:
136
+ """Whether a trigger belongs to a TABLE (`DROP TRIGGER x ON t`) or is global (SQLite: `DROP TRIGGER x`).
137
+ Different syntax, not an absent capability: it is translated in the emitter, the plan does not stop."""
138
+ ...
139
+
140
+ @property
141
+ def max_bind_params(self) -> int:
142
+ """Ceiling of placeholders per statement; the bulk INSERT slices into batches with it. Postgres: 65535."""
143
+ ...
144
+
145
+ @property
146
+ def max_numeric_precision(self) -> int | None:
147
+ """TOTAL digits a NUMERIC/DECIMAL accepts. Postgres: 1000. MySQL: 65.
148
+ `None` if the engine does not restrict it because it IGNORES the declared parameter (SQLite,
149
+ which has per-column affinity): there, any number would assert a limit that does not exist."""
150
+ ...
151
+
152
+ @property
153
+ def max_numeric_scale(self) -> int | None:
154
+ """Decimal places a NUMERIC/DECIMAL accepts. It is NOT the same number as the precision: MySQL
155
+ stops at 30 with a precision of 65, so `DECIMAL(40,35)` has one valid half and one that is not."""
156
+ ...
157
+
158
+ @property
159
+ def max_fractional_seconds(self) -> int | None:
160
+ """Fractional-second digits of a date column. Postgres and MySQL: 6 (microseconds, the
161
+ resolution of Python's `datetime`). SQL Server reaches 7 and Oracle 9 — that is why the
162
+ number belongs to the engine and not to the model."""
163
+ ...
164
+
165
+ def placeholder(self, index: int) -> str:
166
+ """Returns the parameter marker for the given position (e.g. '%s', '$1', '?')."""
167
+ ...
168
+
169
+ def trigger_statements(self, name: str, body: str) -> tuple[list[str], str]:
170
+ """How this engine spells a trigger body: `(statements to run first, the body to inline)`.
171
+
172
+ THE THREE ARE DIFFERENT FROM EACH OTHER, which is why this is a translation and not a flag.
173
+ Measured, all three:
174
+
175
+ PostgreSQL the body goes in a FUNCTION and the trigger calls it (`EXECUTE FUNCTION f()`)
176
+ SQLite the body goes between `BEGIN` and `END`; without them it is a syntax error
177
+ MySQL a single statement goes bare
178
+
179
+ Sending it through verbatim made a declaration fail on whichever engine was not the one it
180
+ had been written for, and the DRIVER is what said so — `syntax error at or near "UPDATE"` on
181
+ PostgreSQL, `near "UPDATE": syntax error` on SQLite. Two different complaints about the same
182
+ portable declaration.
183
+
184
+ The first return value is anything that must exist BEFORE the trigger (PostgreSQL's function,
185
+ nothing for the others); the second is what goes after `FOR EACH ROW`.
186
+ """
187
+ ...
188
+
189
+ def quote_ident(self, name: str) -> str:
190
+ """Quotes an identifier (table or column) the way the engine wants it."""
191
+ ...
192
+
193
+ def json_get_sql(
194
+ self, source: str, key_path: tuple[str, ...], as_type: type
195
+ ) -> str:
196
+ """Emits a read INSIDE a JSON document, cast to the declared type.
197
+
198
+ `source` arrives already emitted (a quoted column, or an expression). The three engines spell
199
+ this so differently —`->>` and a `{a,b}` path, `JSON_EXTRACT` with `$.a` and an unquote, a
200
+ bare `json_extract`— that it belongs here for the same reason placeholders do: what the SQL
201
+ SAYS is the dialect's business, what it MEANS is the graph's.
202
+
203
+ The path is emitted INSIDE a literal because no engine takes a placeholder there. The keys
204
+ are validated when the expression is BUILT (`SnakeValue.json_get`), which is why this may
205
+ interpolate them.
206
+ """
207
+ ...
208
+
209
+ def date_shift_sql(
210
+ self, source: str, amount_placeholder: str, unit: str, keeps_time: bool
211
+ ) -> str:
212
+ """Emits a date moved by a signed amount, with the amount as a PARAMETER.
213
+
214
+ The clearest case in the ORM for this seam: the three spellings have nothing in common
215
+ (`+ INTERVAL`, `DATE_ADD`, a modifier string), and all three were measured to accept the
216
+ amount as a placeholder, so the rule that values never touch the statement survives.
217
+
218
+ `unit` arrives as the agnostic lowercase name (`day`, `month`); each dialect shapes it.
219
+ `keeps_time` says whether the source carries a clock, which only SQLite needs — it has no
220
+ date type to inspect, and picking `date()` for a timestamp would silently drop the time.
221
+ """
222
+ ...
223
+
224
+ def string_agg_sql(
225
+ self, value: str, separator: str, order_by: str, params: list[object]
226
+ ) -> str:
227
+ """Emits a group joined into one string, deciding how the SEPARATOR travels.
228
+
229
+ It takes `params` for the same reason `limit_offset` does: whether the separator can be a
230
+ placeholder is the ENGINE's answer, not the emitter's. Measured — PostgreSQL and SQLite take
231
+ it as a normal argument and parameterise it; MySQL makes it the `SEPARATOR` keyword and
232
+ rejects a placeholder there, so that dialect escapes it through `literal()`.
233
+
234
+ `order_by` arrives already emitted, or empty. It is worth passing whenever a person reads the
235
+ result: without it the order belongs to the engine and can change between runs.
236
+ """
237
+ ...
238
+
239
+ def integer_division_op(self) -> str:
240
+ """How this engine spells division between two INTEGERS.
241
+
242
+ MEASURED, and the three do not agree: `SELECT 45/50` answers `0` on PostgreSQL and SQLite and
243
+ `0.9000` on MySQL, whose `/` is decimal division and which keeps `DIV` as a separate operator
244
+ for the integer one. The ORM declares `SnakeArith[int]` for two integer columns, so without
245
+ this the declared type is simply false on one engine of three.
246
+
247
+ It is asked ONLY when both operands are provably integers. Anything unproven keeps `/`.
248
+ """
249
+ ...
250
+
251
+ def cast_sql(self, source: str, as_type: type) -> str:
252
+ """Emits an EXPLICIT cast of an already-emitted value to the named type.
253
+
254
+ It lives here for the same reason `json_get_sql` does, and the reason was MEASURED rather
255
+ than assumed: `CAST(x AS NUMERIC)` answers `0.9` on PostgreSQL and `0` on SQLite, whose
256
+ NUMERIC affinity collapses an integral value back to an integer. A single spelling shared by
257
+ the three engines would be right on two and silently wrong on the third.
258
+
259
+ The type is guaranteed to be in `CASTABLE`: `snake_cast` refuses anything else at the call
260
+ site, so an implementation never has to answer for a type it cannot spell.
261
+ """
262
+ ...
263
+
264
+ def map_type(
265
+ self,
266
+ python_type: object,
267
+ autoincrement: bool = False,
268
+ params: SnakeTypeParams | None = None,
269
+ ) -> str:
270
+ """Translates a Python type into its COMPLETE SQL type, with its family's parameters.
271
+
272
+ `params` arrives as ONE object per family (an int's width, a str's length, a Decimal's
273
+ precision, a dict's backing) and not as five loose knobs: loose, `precision` ended up
274
+ OUTSIDE this method —it was concatenated onto the result with an f-string— and that is why
275
+ it was the only parameter that was never validated. What this returns is the whole type,
276
+ not a fragment.
277
+
278
+ Returning the complete type is also what lets each engine genuinely decide: Postgres honours
279
+ every parameter, SQLite ignores the ones it does not distinguish.
280
+ """
281
+ ...
282
+
283
+ def drop_all_sql(self, tables: Sequence[str]) -> tuple[str, ...]:
284
+ """Statements that leave the schema empty of `tables`, in the order they must run.
285
+
286
+ Emptying a schema is DDL, so it is written here like the rest of it. The keyword was never
287
+ the hard part: what differs is how each engine is persuaded to ignore the foreign keys while
288
+ the tables come down — Postgres cascades per statement, MySQL has a session switch, SQLite
289
+ has a pragma. Three answers to one question is what a dialect is for.
290
+
291
+ It lives in the Protocol so a fourth engine cannot arrive without answering it. The CLI's
292
+ `fresh` used to write `DROP TABLE ... CASCADE` itself, which is Postgres and only Postgres,
293
+ so the one DESTRUCTIVE command failed halfway on the other two.
294
+
295
+ Empty in, empty out: with no tables there is nothing to bracket either.
296
+ """
297
+ ...
298
+
299
+ def explain_sql(self, sql: str) -> str:
300
+ """Wraps a statement so the engine reports its PLAN instead of running it.
301
+
302
+ It lives here and not in the driver because the difference is grammar, not execution:
303
+ `EXPLAIN` on two engines, `EXPLAIN QUERY PLAN` on SQLite. The compiled `(sql, params)` goes
304
+ down the existing `fetch_all`, so nothing in the driver Protocol moves for this.
305
+
306
+ The ANSWER has no common shape and is not given one: Postgres returns one column, SQLite
307
+ four, MySQL about a dozen. The session hands back the engine's own lines.
308
+ """
309
+ ...
310
+
311
+ def statement_timeout_sql(self, milliseconds: int) -> str | None:
312
+ """The statement that caps how long a query may run, or `None` if the engine has none.
313
+
314
+ One hung query drains a pool, so this is a production knob rather than a nicety — and it
315
+ was written INSIDE `TimeoutDriver` as `SET statement_timeout = <ms>`, which is Postgres and
316
+ only Postgres, under a class name that promises nothing about engines. Measured: MySQL
317
+ answers `1193 Unknown system variable` and SQLite a syntax error.
318
+
319
+ `None` is a legitimate answer and not a gap: SQLite has no server-side statement timeout at
320
+ all. The caller refuses out loud rather than pretending; inventing something plausible there
321
+ would be answering a different question.
322
+
323
+ The value is in MILLISECONDS because that is the unit the API is written in. An engine whose
324
+ variable expects another unit converts here, which is exactly the kind of thing a dialect is
325
+ for.
326
+ """
327
+ ...
328
+
329
+ def register_type(self, python_type: object, sql_type: str) -> None:
330
+ """Adds (or rewrites) the SQL spelling of a Python type in THIS dialect.
331
+
332
+ The extension point of the type vocabulary: without it, declaring an `INET`, a `CITEXT` or a
333
+ domain type meant editing the dialect — the type system was the single source of truth but
334
+ you could not add words to it. It is per dialect because the same Python type is written
335
+ differently on each engine.
336
+ """
337
+ ...
338
+
339
+ def literal(self, value: object) -> str:
340
+ """Formats a value as a SQL literal for DDL (DEFAULT), which takes no placeholders.
341
+ The formatting (TRUE vs 1, quoting) varies between engines, which is why it lives in the dialect.
342
+ """
343
+ ...
344
+
345
+ def function_name(self, func: SnakeFunc) -> str:
346
+ """Translates the agnostic name of a scalar function into the engine's own."""
347
+ ...
348
+
349
+ def index_method(self, method: SnakeIndexMethod) -> str:
350
+ """Translates an index's access method (agnostic) into the engine's jargon."""
351
+ ...
352
+
353
+ def server_default_sql(self, value: SnakeServerDefault) -> str:
354
+ """Translates a SERVER-side default value (agnostic: `NOW`, `UUID_V4`...) into its SQL
355
+ expression on the engine. If it cannot translate it, it raises `SnakeDialectError`.
356
+ """
357
+ ...
358
+
359
+ def limit_offset(
360
+ self, limit: int | None, offset: int | None, params: list[object]
361
+ ) -> str:
362
+ """Emits the parametrised LIMIT/OFFSET clause (appending to `params`), or '' if there is neither.
363
+ Non-standard syntax: that is why the dialect decides it.
364
+ """
365
+ ...
366
+
367
+ def on_conflict_clause(
368
+ self, conflict_columns: Sequence[str], update_columns: Sequence[str]
369
+ ) -> str:
370
+ """An upsert's conflict-resolution clause. `conflict_columns` define the conflict (a
371
+ UNIQUE/PK constraint); `update_columns` are rewritten with the incoming value, or empty → leave alone.
372
+ """
373
+ ...