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,417 @@
1
+ """How the SQL is written for SQLite. It supports almost all of `query/` (RETURNING, ON CONFLICT,
2
+ row constructor, WITH RECURSIVE, window functions).
3
+
4
+ What it does NOT have, and what it has only HALFWAY, is declared in `capabilities`: the absences
5
+ (`ADD CONSTRAINT`, `ALTER COLUMN`, `CREATE SCHEMA`, `FOR UPDATE`, `COMMENT ON`) fail at COMPILE time
6
+ with the alternative —SQL the engine would not understand is never emitted—, and the degradations (a
7
+ `Decimal` that comes back exact but sorts as text) are warned about when the session opens, once
8
+ each.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Sequence
14
+ from datetime import date, datetime, time, timedelta
15
+ from decimal import Decimal
16
+ from enum import Enum
17
+ from typing import get_origin
18
+ from uuid import UUID
19
+
20
+ from snakeorm.dialects.literals import numeric_literal
21
+ from snakeorm.core.exceptions import SnakeDialectError
22
+ import sqlite3
23
+
24
+ from snakeorm.dialects.matrix import Engine, capabilities_for
25
+ from snakeorm.dialects.capabilities import (
26
+ AlterColumnStyle,
27
+ CommentStyle,
28
+ EmptyInsertStyle,
29
+ DerivedFlags,
30
+ SnakeLimits,
31
+ SnakeSyntax,
32
+ )
33
+ from snakeorm.times import SnakeUtc
34
+ from snakeorm.expressions import SnakeFunc
35
+ from snakeorm.metadata import (
36
+ SnakeIndexMethod,
37
+ SnakeServerDefault,
38
+ SnakeTypeParams,
39
+ )
40
+
41
+ _SQLITE_TYPES: dict[object, str] = {
42
+ SnakeUtc: "TEXT",
43
+ # SQLite has FIVE storage classes; everything lands in one. The engine tells nothing else apart.
44
+ int: "INTEGER",
45
+ bool: "INTEGER", # 0/1: SQLite has no boolean
46
+ str: "TEXT",
47
+ float: "REAL",
48
+ bytes: "BLOB",
49
+ # TEXT, not NUMERIC: the NUMERIC affinity converts any numeric text to REAL, destroying the
50
+ # exactness Decimal exists to preserve. With TEXT it comes back exact.
51
+ Decimal: "TEXT",
52
+ # No date types: ISO-8601 text, the only thing that sorts and compares properly (SQLite recommends it).
53
+ datetime: "TEXT",
54
+ date: "TEXT",
55
+ time: "TEXT",
56
+ timedelta: "TEXT",
57
+ UUID: "TEXT",
58
+ dict: "TEXT", # JSON as text: SQLite's json_* functions operate on TEXT
59
+ }
60
+
61
+ _SQLITE_FUNCTIONS: dict[SnakeFunc, str] = {
62
+ SnakeFunc.LOWER: "LOWER",
63
+ SnakeFunc.UPPER: "UPPER",
64
+ SnakeFunc.TRIM: "TRIM",
65
+ SnakeFunc.LENGTH: "LENGTH",
66
+ SnakeFunc.CONCAT: "CONCAT",
67
+ SnakeFunc.SUBSTRING: "SUBSTRING",
68
+ SnakeFunc.REPLACE: "REPLACE",
69
+ SnakeFunc.ABS: "ABS",
70
+ SnakeFunc.ROUND: "ROUND",
71
+ # `CEIL`, `FLOOR`, `SQRT` and `POWER` are a COMPILE-TIME option (`ENABLE_MATH_FUNCTIONS`); a
72
+ # build without it answers `no such function: ceil` at runtime. That is a property of the
73
+ # BINARY, not of "SQLite", so it cannot be a `Cap`: a capability is answered by the class, which
74
+ # knows nothing about which library got linked. `ABS` and `ROUND` are NOT in that group — every
75
+ # build ships them, which is why their absence here was a bug and not a limit.
76
+ SnakeFunc.CEIL: "CEIL",
77
+ SnakeFunc.FLOOR: "FLOOR",
78
+ SnakeFunc.SQRT: "SQRT",
79
+ SnakeFunc.POWER: "POWER",
80
+ }
81
+
82
+ _SQLITE_CANNOT: dict[SnakeFunc, str] = {
83
+ SnakeFunc.DATE_TRUNC: "SQLite has no DATE_TRUNC.",
84
+ SnakeFunc.EXTRACT: "SQLite has no EXTRACT.",
85
+ }
86
+ """What the engine genuinely cannot do, and why.
87
+
88
+ Declared rather than left out, because a function missing from the table above reads exactly like
89
+ one nobody got round to. That is how `ABS` and `ROUND` sat absent while Postgres and MySQL had both:
90
+ silence meant two different things and nothing could tell them apart.
91
+ """
92
+
93
+ _missing = set(SnakeFunc) - set(_SQLITE_FUNCTIONS) - set(_SQLITE_CANNOT)
94
+ if (
95
+ _missing
96
+ ): # pragma: no cover - the guard exists so this can never be reached in a good build
97
+ raise SnakeDialectError(
98
+ "SQLiteDialect does not answer for every scalar function: "
99
+ f"{sorted(f.name for f in _missing)}. Add it to _SQLITE_FUNCTIONS, or to _SQLITE_CANNOT "
100
+ "with the reason. Like the `Cap` catalogue, forgetting one fails at import."
101
+ )
102
+
103
+ _SQLITE_SERVER_DEFAULTS: dict[SnakeServerDefault, str] = {
104
+ SnakeServerDefault.NOW: "CURRENT_TIMESTAMP",
105
+ }
106
+
107
+
108
+ class SQLiteDialect(DerivedFlags):
109
+ """How the SQL is written for SQLite (with the stdlib's `sqlite3` driver)."""
110
+
111
+ capabilities = capabilities_for(Engine.SQLITE, sqlite3.sqlite_version_info)
112
+
113
+ syntax = SnakeSyntax(
114
+ # A SQLite trigger is GLOBAL, not per table: `DROP TRIGGER x` without `ON table` (Postgres's
115
+ # shape gave `near "ON": syntax error` and left ORM triggers the ORM could not drop).
116
+ triggers_are_table_scoped=False,
117
+ indexes_are_table_scoped=False, # `DROP INDEX x`, without `ON table`
118
+ empty_insert_style=EmptyInsertStyle.DEFAULT_VALUES,
119
+ # `NULLS FIRST|LAST` since SQLite 3.30; measured on 3.50.4, which is what this
120
+ # interpreter carries. Not shared with MySQL, which answers 1064 to the same words.
121
+ has_nulls_ordering=True,
122
+ alter_column_style=AlterColumnStyle.UNSUPPORTED,
123
+ # It stores no comment at all, so there is no spelling to pick. Same as the line above: the
124
+ # plan stops on `Cap.COMMENTS` before any emitter runs.
125
+ comment_style=CommentStyle.UNSUPPORTED,
126
+ # MEASURED, and it is not the pragma the design named. `PRAGMA foreign_keys = OFF` is
127
+ # documented as a NO-OP inside a transaction, and `SQLiteDriver` opens one lazily before the
128
+ # first statement — so a rebuild that trusted it would run with the keys still armed and
129
+ # nothing would say so. `defer_foreign_keys` is the one built for this: it is set INSIDE the
130
+ # transaction, it resets itself at COMMIT, and the violations it postpones are reported by
131
+ # that COMMIT.
132
+ defer_constraints_statement="PRAGMA defer_foreign_keys = ON",
133
+ )
134
+
135
+ # `None`, and not some big number: SQLite has no ceilings to break because it does NOT STORE the
136
+ # declared parameter —it has a per-column affinity and nothing else—. A number would assert a
137
+ # limit that does not exist; a small one would reject models this engine stores just as well.
138
+ limits = SnakeLimits(
139
+ bind_params=32766, # the default SQLITE_MAX_VARIABLE_NUMBER since 3.32
140
+ numeric_precision=None,
141
+ numeric_scale=None,
142
+ fractional_seconds=None,
143
+ )
144
+
145
+ def __init__(self) -> None:
146
+ # A PER-INSTANCE copy of the type table: `register_type` writes here, not into the module's
147
+ # dict. Global, importing a library that registers a type would sneak it into every dialect
148
+ # in the process, including those of a database that does not support it.
149
+ self._types: dict[object, str] = dict(_SQLITE_TYPES)
150
+
151
+ def drop_all_sql(self, tables: Sequence[str]) -> tuple[str, ...]:
152
+ """Plain drops, with the key checking POSTPONED to the COMMIT.
153
+
154
+ No `CASCADE`: SQLite does not parse the keyword and answers a syntax error.
155
+
156
+ And not `PRAGMA foreign_keys = OFF` either. MEASURED, it makes nothing work: the pragma is a
157
+ no-op inside a transaction — the same finding written twenty lines above, about
158
+ `defer_constraints_statement` — and `SQLiteDriver._ensure_tx` opens one before every
159
+ statement that reaches this engine, so the value still reads 1 immediately after the OFF is
160
+ sent.
161
+
162
+ `defer_foreign_keys` is the pragma built for this and the one `syntax` already names: it
163
+ takes effect INSIDE the transaction and moves every check to the COMMIT, by which point
164
+ every table is gone and there is nothing left to violate. That also covers the case no
165
+ ordering can reach — two tables pointing at each other — which is what Postgres's `CASCADE`
166
+ and MySQL's switch survive today. And it resets ITSELF at that COMMIT, so unlike MySQL's
167
+ switch there is nothing to put back: no session is ever handed over with its keys disarmed.
168
+
169
+ It is a COMMIT-time promise, so the one thing it asks of the caller is the one thing the
170
+ Protocol already asks: run the batch as it comes. A caller that committed between two drops
171
+ would be back to needing a safe order — which is what `fresh` used to do, and it died here
172
+ with `FOREIGN KEY constraint failed` halfway through, schema half gone, because a model
173
+ declared BEFORE the one pointing at it is perfectly legal.
174
+ """
175
+ if not tables:
176
+ return ()
177
+ drops = tuple(
178
+ f"DROP TABLE IF EXISTS {self.quote_ident(table)}" for table in tables
179
+ )
180
+ defer = self.syntax.defer_constraints_statement
181
+ return ((defer,) if defer is not None else ()) + drops
182
+
183
+ def explain_sql(self, sql: str) -> str:
184
+ """`EXPLAIN QUERY PLAN`, and the two words matter.
185
+
186
+ A bare `EXPLAIN` here dumps the VDBE bytecode — a real answer to a different question, and
187
+ the one a user reading "explain" never wants.
188
+ """
189
+ return f"EXPLAIN QUERY PLAN {sql}"
190
+
191
+ def statement_timeout_sql(self, milliseconds: int) -> str | None:
192
+ """None: SQLite has no server-side statement timeout, and `busy_timeout` is not one.
193
+
194
+ `busy_timeout` waits for a LOCK to be released; it does nothing about a query that is simply
195
+ slow. Returning it here would be answering a different question with a value that looks
196
+ right, which is worse than answering nothing.
197
+ """
198
+ return None
199
+
200
+ def register_type(self, python_type: object, sql_type: str) -> None:
201
+ """Adds (or rewrites) the SQL spelling of a Python type in THIS dialect.
202
+
203
+ See `SnakeDialect.register_type`. It goes per dialect because the same Python type is
204
+ written differently on each engine: an `Inet` is `INET` on Postgres and `TEXT` here.
205
+ """
206
+ self._types[python_type] = sql_type
207
+
208
+ def placeholder(self, index: int) -> str:
209
+ """SQLite uses a positional '?' (like psycopg2's `%s`): params in TEXTUAL ORDER for both."""
210
+ return "?"
211
+
212
+ def trigger_statements(self, name: str, body: str) -> tuple[list[str], str]:
213
+ """`BEGIN ... END` around the body. Without them SQLite answers `near "UPDATE": syntax error`.
214
+
215
+ Measured: it is not optional even for a single statement, which is the difference from MySQL
216
+ and the reason this is three implementations and not two.
217
+
218
+ A body that ALREADY carries them is left alone, the same rule PostgreSQL applies to a body
219
+ that already calls a function. Wrapping blindly produced `BEGIN BEGIN SELECT 1; END END` and
220
+ SQLite answered `near "BEGIN": syntax error` — the asymmetry was written into this very
221
+ method, in the same session that had just fixed it one file over.
222
+ """
223
+ stripped = body.strip()
224
+ if stripped.upper().startswith("BEGIN"):
225
+ return [], stripped
226
+ return [], f"BEGIN {body} END"
227
+
228
+ def quote_ident(self, name: str) -> str:
229
+ """Quotes with double quotes and doubles the inner ones (SQLite accepts the standard)."""
230
+ escaped = name.replace('"', '""')
231
+ return f'"{escaped}"'
232
+
233
+ def json_get_sql(
234
+ self, source: str, key_path: tuple[str, ...], as_type: type
235
+ ) -> str:
236
+ """`json_extract` with a `$.a.b` path. The CAST is kept even though SQLite types the result.
237
+
238
+ Keeping it is not belt and braces: SQLite's `json_extract` gives back whatever the document
239
+ held, so a `"5"` stored as text comes back as text and the comparison would be lexicographic
240
+ — the very failure the declared type exists to stop, and the one SQLite is most prone to.
241
+ """
242
+ path = "$." + ".".join(key_path)
243
+ access = f"json_extract({source}, '{path}')"
244
+ cast = _SQLITE_CAST.get(as_type)
245
+ return access if cast is None else f"CAST({access} AS {cast})"
246
+
247
+ def date_shift_sql(
248
+ self, source: str, amount_placeholder: str, unit: str, keeps_time: bool
249
+ ) -> str:
250
+ """`date(col, ? || ' days')` — the modifier is TEXT, built in SQL and never in Python.
251
+
252
+ `keeps_time` is what picks the function, and it is the only dialect that needs it: SQLite has
253
+ no date type to inspect, and `date()` on a timestamp would silently drop the clock. The
254
+ compiled type stamped on the expression is what answers it.
255
+
256
+ The plural is not cosmetic: SQLite's modifiers are `days`, `months`, `years`.
257
+ """
258
+ function = "datetime" if keeps_time else "date"
259
+ return f"{function}({source}, {amount_placeholder} || ' {unit}s')"
260
+
261
+ def string_agg_sql(
262
+ self, value: str, separator: str, order_by: str, params: list[object]
263
+ ) -> str:
264
+ """`group_concat(col, ? ORDER BY ...)`: a different NAME, the same shape as Postgres.
265
+
266
+ The `ORDER BY` inside the call only arrived in SQLite 3.44, which is why it was measured
267
+ rather than assumed.
268
+ """
269
+ params.append(separator)
270
+ placeholder = self.placeholder(len(params))
271
+ inside = f"{value}, {placeholder}"
272
+ return (
273
+ f"group_concat({inside} ORDER BY {order_by})"
274
+ if order_by
275
+ else f"group_concat({inside})"
276
+ )
277
+
278
+ def integer_division_op(self) -> str:
279
+ """`/` — measured: `SELECT 45/50` is `0` here too. SQLite agrees with PostgreSQL on this."""
280
+ return "/"
281
+
282
+ def cast_sql(self, source: str, as_type: type) -> str:
283
+ """`CAST(x AS REAL)` — and REAL is the point, not a detail.
284
+
285
+ MEASURED: `CAST(45 AS NUMERIC) / 50` answers `0` here and `0.9` on PostgreSQL. SQLite's
286
+ NUMERIC affinity converts back to an integer when the value is integral, so the tempting
287
+ single spelling for the three engines would lose the decimals on this one, in silence.
288
+ """
289
+ return f"CAST({source} AS {_SQLITE_CAST[as_type]})"
290
+
291
+ def map_type(
292
+ self,
293
+ python_type: object,
294
+ autoincrement: bool = False,
295
+ params: SnakeTypeParams | None = None,
296
+ ) -> str:
297
+ """Translates the Python type into one of SQLite's five storage classes.
298
+
299
+ With `autoincrement` it returns `INTEGER`: SQLite's autoincrement is `INTEGER PRIMARY KEY`
300
+ (an alias of ROWID). The `params` are accepted but IGNORED: SQLite has a single affinity per
301
+ class, so a width, a length or a precision have nowhere to be written. They are accepted so
302
+ that the same model works on both engines; that they change nothing here is said by the
303
+ dialect's fidelity warning, not by silence.
304
+ """
305
+ if autoincrement:
306
+ return "INTEGER"
307
+ origin = get_origin(python_type)
308
+ if origin is not None:
309
+ # SQLite has no arrays: the list falls back to TEXT and travels as JSON (`adapt_param`
310
+ # serialises it, `_to_list` rebuilds it, so the attribute is still a list). It used to
311
+ # be REJECTED so as not to lose the type in silence, and that was right while there was
312
+ # no way to say so; with the degradation declared in `capabilities` there is one, and
313
+ # rejecting only achieved that a list column existed on Postgres alone.
314
+ if origin is list:
315
+ return "TEXT"
316
+ if origin is dict:
317
+ return "TEXT"
318
+ if isinstance(python_type, type) and issubclass(python_type, Enum):
319
+ # An enum is stored as its BASE type (str or int) with its CHECK alongside, as on Postgres.
320
+ base = str if issubclass(python_type, str) else int
321
+ return _SQLITE_TYPES[base]
322
+ try:
323
+ return self._types[python_type]
324
+ except (KeyError, TypeError):
325
+ raise SnakeDialectError(
326
+ f"SQLiteDialect does not know how to translate {python_type!r} into a SQLite type"
327
+ ) from None
328
+
329
+ def limit_offset(
330
+ self, limit: int | None, offset: int | None, params: list[object]
331
+ ) -> str:
332
+ """A parametrised `LIMIT ? OFFSET ?`. With `offset` and no `limit` it uses `LIMIT -1`: SQLite does not accept a bare OFFSET."""
333
+ parts: list[str] = []
334
+ if limit is not None:
335
+ params.append(limit)
336
+ parts.append(f"LIMIT {self.placeholder(len(params))}")
337
+ elif offset is not None:
338
+ parts.append("LIMIT -1")
339
+ if offset is not None:
340
+ params.append(offset)
341
+ parts.append(f"OFFSET {self.placeholder(len(params))}")
342
+ return " ".join(parts)
343
+
344
+ def on_conflict_clause(
345
+ self, conflict_columns: Sequence[str], update_columns: Sequence[str]
346
+ ) -> str:
347
+ """`ON CONFLICT (<cols>) DO NOTHING` / `DO UPDATE SET c = excluded.c`. Like Postgres, but with `excluded` in lower case."""
348
+ conflict = ", ".join(self.quote_ident(column) for column in conflict_columns)
349
+ if not update_columns:
350
+ return f"ON CONFLICT ({conflict}) DO NOTHING"
351
+ assignments = ", ".join(
352
+ f"{self.quote_ident(column)} = excluded.{self.quote_ident(column)}"
353
+ for column in update_columns
354
+ )
355
+ return f"ON CONFLICT ({conflict}) DO UPDATE SET {assignments}"
356
+
357
+ def literal(self, value: object) -> str:
358
+ """Formats a value as a SQLite SQL literal (DDL, no params).
359
+ A bool goes as 0/1: SQLite has no boolean type, it stores integers.
360
+ """
361
+ if isinstance(value, Enum):
362
+ value = value.value
363
+ if value is None:
364
+ return "NULL"
365
+ if isinstance(value, bool): # before int: bool is a subclass of int
366
+ return "1" if value else "0"
367
+ if isinstance(value, (int, float, Decimal)):
368
+ return numeric_literal(value, "SQLiteDialect")
369
+ if isinstance(value, str):
370
+ escaped = value.replace("'", "''")
371
+ return f"'{escaped}'"
372
+ raise SnakeDialectError(
373
+ f"SQLiteDialect does not know how to format {value!r} as a SQL literal"
374
+ )
375
+
376
+ def function_name(self, func: SnakeFunc) -> str:
377
+ """Translates the agnostic name of a scalar function into SQLite's own."""
378
+ if func in _SQLITE_FUNCTIONS:
379
+ return _SQLITE_FUNCTIONS[func]
380
+ reason = _SQLITE_CANNOT.get(func)
381
+ if reason is None:
382
+ # Not a catalogue member at all. A real one cannot reach here: the import
383
+ # guard above refuses to load a dialect that skipped any.
384
+ raise SnakeDialectError(
385
+ f"SQLiteDialect does not know how to translate the function {func!r}"
386
+ )
387
+ raise SnakeDialectError(
388
+ f"SQLiteDialect cannot translate {func.name}: {reason} "
389
+ "Reach for it through `raw()` with the engine's own spelling."
390
+ )
391
+
392
+ def index_method(self, method: SnakeIndexMethod) -> str:
393
+ """SQLite has ONE kind of index: there is no `USING`.
394
+ It is rejected instead of ignored: accepting `method=GIN` and emitting a plain index would lie in silence.
395
+ """
396
+ raise SnakeDialectError(
397
+ f"SQLite has only one kind of index, so it does not accept method={method.name}. "
398
+ f"Drop the `method=` for this engine."
399
+ )
400
+
401
+ def server_default_sql(self, value: SnakeServerDefault) -> str:
402
+ """Translates the server-side default into its SQLite expression."""
403
+ try:
404
+ return _SQLITE_SERVER_DEFAULTS[value]
405
+ except KeyError:
406
+ raise SnakeDialectError(
407
+ f"SQLiteDialect does not know how to translate the server_default {value!r} to SQL"
408
+ ) from None
409
+
410
+ def __repr__(self) -> str:
411
+ """The dialect's name, useful in the capability error messages."""
412
+ return "SQLiteDialect()"
413
+
414
+
415
+ _SQLITE_CAST = {int: "INTEGER", float: "REAL", bool: "INTEGER"}
416
+ """What a declared type becomes in a SQLite cast. `str` is absent: the value is already text,
417
+ and SQLite has no boolean, so a bool is the INTEGER it stores."""
@@ -0,0 +1,18 @@
1
+ """SnakeORM drivers: how the SQL is EXECUTED for each engine."""
2
+
3
+ from snakeorm.drivers.asyncbase import AsyncDriver as AsyncDriver
4
+ from snakeorm.drivers.asyncdecorators import AsyncLoggingDriver as AsyncLoggingDriver
5
+ from snakeorm.drivers.asyncdecorators import AsyncTimeoutDriver as AsyncTimeoutDriver
6
+ from snakeorm.drivers.asyncpsycopg import AsyncPsycopgDriver as AsyncPsycopgDriver
7
+ from snakeorm.drivers.asyncpymysql import AsyncPyMySQLDriver as AsyncPyMySQLDriver
8
+ from snakeorm.drivers.asyncsqlite import AsyncSQLiteDriver as AsyncSQLiteDriver
9
+ from snakeorm.drivers.threaded import ThreadedAsyncDriver as ThreadedAsyncDriver
10
+ from snakeorm.drivers.asyncpool import AsyncSnakePool as AsyncSnakePool
11
+ from snakeorm.drivers.base import SnakeDriver as SnakeDriver
12
+ from snakeorm.drivers.logging import LoggingDriver as LoggingDriver
13
+ from snakeorm.drivers.pool import SnakePool as SnakePool
14
+ from snakeorm.drivers.pool import psycopg_pool as psycopg_pool
15
+ from snakeorm.drivers.psycopg import PsycopgDriver as PsycopgDriver
16
+ from snakeorm.drivers.pymysql import PyMySQLDriver as PyMySQLDriver
17
+ from snakeorm.drivers.sqlite import SQLiteDriver as SQLiteDriver
18
+ from snakeorm.drivers.timeout import TimeoutDriver as TimeoutDriver
@@ -0,0 +1,73 @@
1
+ """`AsyncDriver`: the async mirror of `SnakeDriver` (same methods, `async`). The only piece
2
+ with "colour" on the execution side: SQL generation has none, so the compiler, the dialect,
3
+ the query, the expressions and the migrations are reused AS THEY ARE between sync and async.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import AsyncIterator, Sequence
9
+ from typing import Protocol
10
+
11
+
12
+ class AsyncDriver(Protocol):
13
+ """How the SQL is EXECUTED asynchronously. It cannot write it: that is the dialect."""
14
+
15
+ async def fetch_all(
16
+ self, sql: str, params: Sequence[object]
17
+ ) -> list[tuple[object, ...]]:
18
+ """Runs the query and returns every row as tuples."""
19
+ ...
20
+
21
+ def fetch_iter(
22
+ self, sql: str, params: Sequence[object], *, chunk: int = 1000
23
+ ) -> AsyncIterator[tuple[object, ...]]:
24
+ """Yields the rows WITHOUT materialising them all. Mirror of `SnakeDriver.fetch_iter`.
25
+
26
+ It is NOT an `async def`: it returns the async iterator directly. An `async def` that
27
+ were also a generator would force an `await` before the `async for`, and the contract
28
+ would end up different from the synchronous one for no reason at all.
29
+ """
30
+ ...
31
+
32
+ async def execute(self, sql: str, params: Sequence[object]) -> int:
33
+ """Runs a statement fetching no rows; returns how many it affected."""
34
+ ...
35
+
36
+ @property
37
+ def last_insert_id(self) -> int:
38
+ """The autoincrement id of the last INSERT. Exact mirror of `SnakeDriver.last_insert_id`.
39
+
40
+ It was missing from this Protocol, and it bothered nobody because the only async driver
41
+ was the Postgres one, where the PK comes back through RETURNING and this is irrelevant.
42
+ It is on MySQL —where there is no RETURNING; MariaDB does have it— that the PK depends on this
43
+ value: without the member in the contract, an async MySQL driver would have been born
44
+ leaving the PK at `None` without saying a word.
45
+
46
+ It is NOT `async`: it does not travel to the database, the cursor of the last write
47
+ stored it.
48
+ """
49
+ ...
50
+
51
+ async def commit(self) -> None:
52
+ """Commits the transaction in progress."""
53
+ ...
54
+
55
+ async def rollback(self) -> None:
56
+ """Rolls back the transaction in progress."""
57
+ ...
58
+
59
+ async def savepoint(self, name: str) -> None:
60
+ """Opens a named savepoint."""
61
+ ...
62
+
63
+ async def release_savepoint(self, name: str) -> None:
64
+ """Commits the savepoint."""
65
+ ...
66
+
67
+ async def rollback_to_savepoint(self, name: str) -> None:
68
+ """Goes back to the savepoint, undoing what was done since it was opened."""
69
+ ...
70
+
71
+ async def close(self) -> None:
72
+ """Closes the connection."""
73
+ ...