pyoq-sql 1.0.0__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 (264) hide show
  1. pyoq/__init__.py +7 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +19 -0
  9. pyoq/config/loader.py +204 -0
  10. pyoq/config/models.py +243 -0
  11. pyoq/descriptors.py +165 -0
  12. pyoq/diagnostics/__init__.py +68 -0
  13. pyoq/diagnostics/budget.py +136 -0
  14. pyoq/diagnostics/events.py +137 -0
  15. pyoq/diagnostics/fingerprint.py +267 -0
  16. pyoq/diagnostics/instrumented.py +237 -0
  17. pyoq/diagnostics/metrics.py +61 -0
  18. pyoq/diagnostics/observation.py +227 -0
  19. pyoq/diagnostics/scoped.py +103 -0
  20. pyoq/django/__init__.py +15 -0
  21. pyoq/django/apps.py +17 -0
  22. pyoq/django/execution.py +317 -0
  23. pyoq/django/generation.py +59 -0
  24. pyoq/django/management/__init__.py +0 -0
  25. pyoq/django/management/commands/__init__.py +0 -0
  26. pyoq/django/management/commands/makemigrations.py +53 -0
  27. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  28. pyoq/django/parameters.py +101 -0
  29. pyoq/django/schema.py +379 -0
  30. pyoq/django/settings.py +87 -0
  31. pyoq/django/timeouts.py +105 -0
  32. pyoq/dsl/__init__.py +64 -0
  33. pyoq/dsl/aio/__init__.py +31 -0
  34. pyoq/dsl/aio/context.py +295 -0
  35. pyoq/dsl/aio/queries.py +335 -0
  36. pyoq/dsl/aio/writes.py +368 -0
  37. pyoq/dsl/context.py +326 -0
  38. pyoq/dsl/entry.py +37 -0
  39. pyoq/dsl/labels.py +36 -0
  40. pyoq/dsl/queries.py +339 -0
  41. pyoq/dsl/result.py +164 -0
  42. pyoq/dsl/writes.py +360 -0
  43. pyoq/errors.py +317 -0
  44. pyoq/fastapi/__init__.py +32 -0
  45. pyoq/fastapi/dependencies.py +167 -0
  46. pyoq/fastapi/lifespan.py +119 -0
  47. pyoq/fetching/__init__.py +55 -0
  48. pyoq/fetching/collections.py +136 -0
  49. pyoq/fetching/execution.py +587 -0
  50. pyoq/fetching/joined.py +79 -0
  51. pyoq/fetching/nesting.py +183 -0
  52. pyoq/fetching/plans.py +541 -0
  53. pyoq/fetching/select_in.py +149 -0
  54. pyoq/fetching/tables.py +110 -0
  55. pyoq/generation/__init__.py +54 -0
  56. pyoq/generation/cleanup.py +44 -0
  57. pyoq/generation/contracts.py +248 -0
  58. pyoq/generation/drift.py +169 -0
  59. pyoq/generation/lock.py +33 -0
  60. pyoq/generation/manifest.py +114 -0
  61. pyoq/generation/model.py +1001 -0
  62. pyoq/generation/pipeline.py +119 -0
  63. pyoq/generation/rendering/__init__.py +5 -0
  64. pyoq/generation/rendering/domains.py +51 -0
  65. pyoq/generation/rendering/enums.py +29 -0
  66. pyoq/generation/rendering/exports.py +70 -0
  67. pyoq/generation/rendering/imports.py +63 -0
  68. pyoq/generation/rendering/package.py +56 -0
  69. pyoq/generation/rendering/relations.py +133 -0
  70. pyoq/generation/rendering/routines.py +396 -0
  71. pyoq/generation/rendering/rows.py +79 -0
  72. pyoq/generation/rendering/source.py +121 -0
  73. pyoq/generation/rendering/tables.py +300 -0
  74. pyoq/generation/rendering/writes.py +514 -0
  75. pyoq/generation/validation.py +27 -0
  76. pyoq/generation/writer.py +184 -0
  77. pyoq/hydration/__init__.py +24 -0
  78. pyoq/hydration/engine.py +155 -0
  79. pyoq/hydration/identity.py +194 -0
  80. pyoq/hydration/plan.py +116 -0
  81. pyoq/migrations/__init__.py +9 -0
  82. pyoq/migrations/alembic.py +106 -0
  83. pyoq/migrations/hooks.py +75 -0
  84. pyoq/naming.py +261 -0
  85. pyoq/policies/__init__.py +47 -0
  86. pyoq/policies/bypass.py +122 -0
  87. pyoq/policies/governed.py +430 -0
  88. pyoq/policies/model.py +242 -0
  89. pyoq/policies/rewriting.py +263 -0
  90. pyoq/py.typed +1 -0
  91. pyoq/query/__init__.py +312 -0
  92. pyoq/query/aggregates.py +172 -0
  93. pyoq/query/arrays.py +65 -0
  94. pyoq/query/binding.py +52 -0
  95. pyoq/query/capabilities.py +317 -0
  96. pyoq/query/casts.py +73 -0
  97. pyoq/query/choices.py +185 -0
  98. pyoq/query/decoding.py +360 -0
  99. pyoq/query/documents.py +56 -0
  100. pyoq/query/execution/__init__.py +63 -0
  101. pyoq/query/execution/aio/__init__.py +31 -0
  102. pyoq/query/execution/aio/operations.py +228 -0
  103. pyoq/query/execution/aio/pooling.py +233 -0
  104. pyoq/query/execution/aio/streaming.py +161 -0
  105. pyoq/query/execution/aio/transactions.py +105 -0
  106. pyoq/query/execution/batch.py +96 -0
  107. pyoq/query/execution/binding_style.py +30 -0
  108. pyoq/query/execution/compilation.py +48 -0
  109. pyoq/query/execution/context.py +61 -0
  110. pyoq/query/execution/control.py +50 -0
  111. pyoq/query/execution/operations.py +224 -0
  112. pyoq/query/execution/planning.py +107 -0
  113. pyoq/query/execution/pooling.py +279 -0
  114. pyoq/query/execution/results.py +36 -0
  115. pyoq/query/execution/streaming.py +178 -0
  116. pyoq/query/execution/transactions.py +95 -0
  117. pyoq/query/expressions.py +1200 -0
  118. pyoq/query/fields.py +60 -0
  119. pyoq/query/mysql/__init__.py +59 -0
  120. pyoq/query/mysql/aio/__init__.py +38 -0
  121. pyoq/query/mysql/aio/commands.py +389 -0
  122. pyoq/query/mysql/aio/driver.py +196 -0
  123. pyoq/query/mysql/aio/executor.py +123 -0
  124. pyoq/query/mysql/aio/factory.py +26 -0
  125. pyoq/query/mysql/aio/operations.py +38 -0
  126. pyoq/query/mysql/aio/pool.py +53 -0
  127. pyoq/query/mysql/aio/transactions.py +313 -0
  128. pyoq/query/mysql/commands.py +354 -0
  129. pyoq/query/mysql/compiler.py +134 -0
  130. pyoq/query/mysql/context.py +20 -0
  131. pyoq/query/mysql/executor.py +126 -0
  132. pyoq/query/mysql/expressions.py +244 -0
  133. pyoq/query/mysql/factory.py +46 -0
  134. pyoq/query/mysql/health.py +66 -0
  135. pyoq/query/mysql/identifiers.py +9 -0
  136. pyoq/query/mysql/model.py +79 -0
  137. pyoq/query/mysql/operations.py +43 -0
  138. pyoq/query/mysql/parameters.py +69 -0
  139. pyoq/query/mysql/planning.py +20 -0
  140. pyoq/query/mysql/pool.py +67 -0
  141. pyoq/query/mysql/transactions.py +331 -0
  142. pyoq/query/mysql/writes.py +73 -0
  143. pyoq/query/nodes.py +750 -0
  144. pyoq/query/postgres/__init__.py +48 -0
  145. pyoq/query/postgres/aio/__init__.py +25 -0
  146. pyoq/query/postgres/aio/bulk.py +56 -0
  147. pyoq/query/postgres/aio/commands.py +264 -0
  148. pyoq/query/postgres/aio/executor.py +152 -0
  149. pyoq/query/postgres/aio/factory.py +26 -0
  150. pyoq/query/postgres/aio/operations.py +26 -0
  151. pyoq/query/postgres/aio/pool.py +40 -0
  152. pyoq/query/postgres/aio/transactions.py +295 -0
  153. pyoq/query/postgres/bulk.py +62 -0
  154. pyoq/query/postgres/commands.py +238 -0
  155. pyoq/query/postgres/compiler.py +114 -0
  156. pyoq/query/postgres/context.py +20 -0
  157. pyoq/query/postgres/executor.py +147 -0
  158. pyoq/query/postgres/expressions.py +311 -0
  159. pyoq/query/postgres/factory.py +24 -0
  160. pyoq/query/postgres/health.py +24 -0
  161. pyoq/query/postgres/identifiers.py +9 -0
  162. pyoq/query/postgres/model.py +81 -0
  163. pyoq/query/postgres/operations.py +25 -0
  164. pyoq/query/postgres/parameters.py +71 -0
  165. pyoq/query/postgres/planning.py +20 -0
  166. pyoq/query/postgres/pool.py +52 -0
  167. pyoq/query/postgres/transactions.py +295 -0
  168. pyoq/query/postgres/writes.py +37 -0
  169. pyoq/query/projections.py +105 -0
  170. pyoq/query/raw.py +90 -0
  171. pyoq/query/recursion.py +265 -0
  172. pyoq/query/rendering/__init__.py +1 -0
  173. pyoq/query/rendering/expressions.py +913 -0
  174. pyoq/query/rendering/identifiers.py +40 -0
  175. pyoq/query/rendering/projections.py +63 -0
  176. pyoq/query/rendering/queries.py +334 -0
  177. pyoq/query/rendering/sources.py +66 -0
  178. pyoq/query/rendering/writes.py +176 -0
  179. pyoq/query/results.py +459 -0
  180. pyoq/query/routines.py +196 -0
  181. pyoq/query/rows.py +156 -0
  182. pyoq/query/select.py +793 -0
  183. pyoq/query/select_nodes.py +277 -0
  184. pyoq/query/sources.py +236 -0
  185. pyoq/query/sqlite/__init__.py +43 -0
  186. pyoq/query/sqlite/commands.py +201 -0
  187. pyoq/query/sqlite/compiler.py +139 -0
  188. pyoq/query/sqlite/context.py +20 -0
  189. pyoq/query/sqlite/executor.py +119 -0
  190. pyoq/query/sqlite/expressions.py +224 -0
  191. pyoq/query/sqlite/factory.py +32 -0
  192. pyoq/query/sqlite/health.py +28 -0
  193. pyoq/query/sqlite/identifiers.py +9 -0
  194. pyoq/query/sqlite/model.py +73 -0
  195. pyoq/query/sqlite/operations.py +36 -0
  196. pyoq/query/sqlite/parameters.py +50 -0
  197. pyoq/query/sqlite/planning.py +20 -0
  198. pyoq/query/sqlite/pool.py +50 -0
  199. pyoq/query/sqlite/streaming.py +13 -0
  200. pyoq/query/sqlite/transactions.py +274 -0
  201. pyoq/query/sqlite/writes.py +35 -0
  202. pyoq/query/statements.py +27 -0
  203. pyoq/query/values.py +23 -0
  204. pyoq/query/vendor.py +162 -0
  205. pyoq/query/windows.py +424 -0
  206. pyoq/query/write_nodes.py +174 -0
  207. pyoq/query/writes.py +628 -0
  208. pyoq/relations/__init__.py +66 -0
  209. pyoq/relations/batching.py +219 -0
  210. pyoq/relations/derivation.py +111 -0
  211. pyoq/relations/fetching.py +355 -0
  212. pyoq/relations/graph.py +245 -0
  213. pyoq/relations/loading.py +74 -0
  214. pyoq/relations/model.py +75 -0
  215. pyoq/relations/planning.py +206 -0
  216. pyoq/runtime/__init__.py +9 -0
  217. pyoq/runtime/kernels.py +25 -0
  218. pyoq/runtime/python.py +43 -0
  219. pyoq/runtime/selection.py +73 -0
  220. pyoq/sanic/__init__.py +32 -0
  221. pyoq/sanic/scope.py +197 -0
  222. pyoq/sanic/workers.py +129 -0
  223. pyoq/schema/__init__.py +108 -0
  224. pyoq/schema/codec.py +711 -0
  225. pyoq/schema/models.py +604 -0
  226. pyoq/schema/mysql/__init__.py +16 -0
  227. pyoq/schema/mysql/connection.py +72 -0
  228. pyoq/schema/mysql/dsn.py +72 -0
  229. pyoq/schema/mysql/records.py +354 -0
  230. pyoq/schema/mysql/reflection.py +309 -0
  231. pyoq/schema/mysql/source.py +30 -0
  232. pyoq/schema/mysql/sql.py +128 -0
  233. pyoq/schema/mysql/types.py +105 -0
  234. pyoq/schema/postgres/__init__.py +13 -0
  235. pyoq/schema/postgres/connection.py +62 -0
  236. pyoq/schema/postgres/records.py +384 -0
  237. pyoq/schema/postgres/reflection.py +466 -0
  238. pyoq/schema/postgres/source.py +30 -0
  239. pyoq/schema/postgres/sql.py +246 -0
  240. pyoq/schema/postgres/types.py +98 -0
  241. pyoq/schema/registry.py +45 -0
  242. pyoq/schema/source.py +15 -0
  243. pyoq/schema/sqlite/__init__.py +6 -0
  244. pyoq/schema/sqlite/connection.py +54 -0
  245. pyoq/schema/sqlite/records.py +167 -0
  246. pyoq/schema/sqlite/reflection.py +393 -0
  247. pyoq/schema/sqlite/source.py +30 -0
  248. pyoq/schema/sqlite/sql.py +254 -0
  249. pyoq/schema/sqlite/types.py +74 -0
  250. pyoq/serving/__init__.py +19 -0
  251. pyoq/serving/databases.py +107 -0
  252. pyoq/snapshots/__init__.py +20 -0
  253. pyoq/snapshots/drift.py +312 -0
  254. pyoq/snapshots/files.py +96 -0
  255. pyoq/snapshots/routing.py +40 -0
  256. pyoq/snapshots/source.py +33 -0
  257. pyoq/tracing/__init__.py +5 -0
  258. pyoq/tracing/spans.py +89 -0
  259. pyoq/unset.py +14 -0
  260. pyoq_sql-1.0.0.dist-info/METADATA +3034 -0
  261. pyoq_sql-1.0.0.dist-info/RECORD +264 -0
  262. pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
  263. pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
  264. pyoq_sql-1.0.0.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,201 @@
1
+ """SQLite cursor execution and cooperative interruption."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from collections.abc import Callable
7
+ from time import monotonic
8
+ from typing import NoReturn, TypeVar, cast
9
+
10
+ from pyoq.errors import QueryCancelledError, QueryExecutionError, QueryTimeoutError
11
+ from pyoq.query.execution import ExecutionControl
12
+ from pyoq.query.sqlite.health import is_connection_failure
13
+ from pyoq.query.sqlite.parameters import SQLiteParameter
14
+
15
+ OperationResult = TypeVar("OperationResult")
16
+ Invalidator = Callable[[], None]
17
+
18
+
19
+ class SQLiteExecutionMonitor:
20
+ __slots__ = ("_control", "_installed", "_started")
21
+
22
+ def __init__(self, control: ExecutionControl | None) -> None:
23
+ self._control = control or ExecutionControl()
24
+ self._started = monotonic() if self._control.timeout is not None else 0.0
25
+ self._installed = False
26
+
27
+ def install(
28
+ self,
29
+ connection: sqlite3.Connection,
30
+ invalidate: Invalidator,
31
+ ) -> None:
32
+ self.checkpoint()
33
+ if not self._requires_progress_handler():
34
+ return
35
+ try:
36
+ connection.set_progress_handler(
37
+ self._progress_handler,
38
+ self._control.progress_steps,
39
+ )
40
+ self._installed = True
41
+ except sqlite3.Error as error:
42
+ _raise_driver_failure(error, invalidate)
43
+
44
+ def checkpoint(self) -> None:
45
+ token = self._control.cancellation_token
46
+ if token is not None and token.cancelled:
47
+ raise QueryCancelledError("SQLite query execution was cancelled")
48
+ timeout = self._control.timeout
49
+ if timeout is not None and monotonic() - self._started >= timeout:
50
+ raise QueryTimeoutError("SQLite query execution exceeded its timeout")
51
+
52
+ def translate_driver_error(
53
+ self,
54
+ error: sqlite3.Error,
55
+ invalidate: Invalidator,
56
+ ) -> NoReturn:
57
+ if is_connection_failure(error):
58
+ invalidate()
59
+ self.checkpoint()
60
+ _raise_query_execution_error(error)
61
+
62
+ def close(
63
+ self,
64
+ connection: sqlite3.Connection,
65
+ invalidate: Invalidator,
66
+ ) -> None:
67
+ if not self._installed:
68
+ return
69
+ self._installed = False
70
+ try:
71
+ connection.set_progress_handler(None, 0)
72
+ except sqlite3.Error:
73
+ invalidate()
74
+
75
+ def _progress_handler(self) -> int:
76
+ return int(self._stopped())
77
+
78
+ def _stopped(self) -> bool:
79
+ token = self._control.cancellation_token
80
+ if token is not None and token.cancelled:
81
+ return True
82
+ timeout = self._control.timeout
83
+ return timeout is not None and monotonic() - self._started >= timeout
84
+
85
+ def _requires_progress_handler(self) -> bool:
86
+ return (
87
+ self._control.timeout is not None
88
+ or self._control.cancellation_token is not None
89
+ )
90
+
91
+
92
+ class SQLiteCursorSession:
93
+ __slots__ = ("_closed", "_connection", "_cursor", "_invalidate", "_monitor")
94
+
95
+ def __init__(
96
+ self,
97
+ connection: sqlite3.Connection,
98
+ cursor: sqlite3.Cursor,
99
+ monitor: SQLiteExecutionMonitor,
100
+ invalidate: Invalidator,
101
+ ) -> None:
102
+ self._connection = connection
103
+ self._cursor = cursor
104
+ self._monitor = monitor
105
+ self._invalidate = invalidate
106
+ self._closed = False
107
+
108
+ @property
109
+ def cursor(self) -> sqlite3.Cursor:
110
+ return self._cursor
111
+
112
+ def checkpoint(self) -> None:
113
+ self._monitor.checkpoint()
114
+
115
+ def translate_driver_error(self, error: sqlite3.Error) -> NoReturn:
116
+ self._monitor.translate_driver_error(error, self._invalidate)
117
+
118
+ def fetch_batch(self, size: int) -> list[tuple[object, ...]]:
119
+ try:
120
+ self._monitor.checkpoint()
121
+ rows = cast(list[tuple[object, ...]], self.cursor.fetchmany(size))
122
+ self._monitor.checkpoint()
123
+ except sqlite3.Error as error:
124
+ self._monitor.translate_driver_error(error, self._invalidate)
125
+ return rows
126
+
127
+ def close(self) -> None:
128
+ if self._closed:
129
+ return
130
+ self._closed = True
131
+ _close_cursor(self._cursor, self._invalidate)
132
+ self._monitor.close(self._connection, self._invalidate)
133
+
134
+
135
+ def run_sql(
136
+ connection: sqlite3.Connection,
137
+ sql: str,
138
+ parameters: tuple[SQLiteParameter, ...],
139
+ operation: Callable[[sqlite3.Cursor], OperationResult],
140
+ control: ExecutionControl | None,
141
+ invalidate: Invalidator,
142
+ ) -> OperationResult:
143
+ session = open_cursor(connection, sql, parameters, control, invalidate)
144
+ try:
145
+ result = operation(session.cursor)
146
+ session.checkpoint()
147
+ return result
148
+ except sqlite3.Error as error:
149
+ session.translate_driver_error(error)
150
+ finally:
151
+ session.close()
152
+
153
+
154
+ def open_cursor(
155
+ connection: sqlite3.Connection,
156
+ sql: str,
157
+ parameters: tuple[SQLiteParameter, ...],
158
+ control: ExecutionControl | None,
159
+ invalidate: Invalidator,
160
+ ) -> SQLiteCursorSession:
161
+ monitor = SQLiteExecutionMonitor(control)
162
+ cursor: sqlite3.Cursor | None = None
163
+ try:
164
+ monitor.install(connection, invalidate)
165
+ cursor = connection.execute(sql, parameters)
166
+ monitor.checkpoint()
167
+ return SQLiteCursorSession(connection, cursor, monitor, invalidate)
168
+ except sqlite3.Error as error:
169
+ if cursor is not None:
170
+ _close_cursor(cursor, invalidate)
171
+ monitor.close(connection, invalidate)
172
+ monitor.translate_driver_error(error, invalidate)
173
+ except BaseException:
174
+ if cursor is not None:
175
+ _close_cursor(cursor, invalidate)
176
+ monitor.close(connection, invalidate)
177
+ raise
178
+
179
+
180
+ def _close_cursor(cursor: sqlite3.Cursor, invalidate: Invalidator) -> None:
181
+ try:
182
+ cursor.close()
183
+ except sqlite3.Error:
184
+ invalidate()
185
+
186
+
187
+ def _raise_driver_failure(
188
+ error: sqlite3.Error,
189
+ invalidate: Invalidator,
190
+ ) -> NoReturn:
191
+ if is_connection_failure(error):
192
+ invalidate()
193
+ _raise_query_execution_error(error)
194
+
195
+
196
+ def _raise_query_execution_error(error: sqlite3.Error) -> NoReturn:
197
+ message = f"SQLite query execution failed: {error}"
198
+ raise QueryExecutionError(message) from error
199
+
200
+
201
+ __all__ = ("SQLiteCursorSession", "open_cursor", "run_sql")
@@ -0,0 +1,139 @@
1
+ """SQLite query and statement compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from pyoq.errors import UnsupportedQueryError
8
+ from pyoq.query.execution import CompiledQuery, StatementKind
9
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
10
+ from pyoq.query.rendering.queries import QueryRenderer
11
+ from pyoq.query.select_nodes import (
12
+ JoinKind,
13
+ LockNode,
14
+ NullOrder,
15
+ QueryNode,
16
+ SelectNode,
17
+ SetNode,
18
+ )
19
+ from pyoq.query.sqlite.context import SQLiteCompilationContext
20
+ from pyoq.query.sqlite.expressions import SQLiteExpressionCompiler
21
+ from pyoq.query.sqlite.identifiers import QUOTING
22
+ from pyoq.query.sqlite.model import SQLiteCapabilities
23
+ from pyoq.query.sqlite.writes import SQLiteWriteCompilation
24
+ from pyoq.query.statements import StatementNode, StatementProvider
25
+ from pyoq.query.write_nodes import CallNode, DeleteNode, InsertNode, UpdateNode
26
+
27
+ _STATEMENT_NODES = (
28
+ SelectNode,
29
+ SetNode,
30
+ InsertNode,
31
+ UpdateNode,
32
+ DeleteNode,
33
+ CallNode,
34
+ )
35
+
36
+
37
+ class QueryProvider(Protocol):
38
+ @property
39
+ def node(self) -> QueryNode: ...
40
+
41
+
42
+ class SQLiteCompiler:
43
+ __slots__ = ("_capabilities",)
44
+
45
+ def __init__(
46
+ self,
47
+ capabilities: SQLiteCapabilities | None = None,
48
+ ) -> None:
49
+ self._capabilities = capabilities or SQLiteCapabilities()
50
+
51
+ @property
52
+ def capabilities(self) -> SQLiteCapabilities:
53
+ return self._capabilities
54
+
55
+ def compile(
56
+ self,
57
+ statement: StatementProvider | StatementNode,
58
+ /,
59
+ ) -> CompiledQuery:
60
+ node = statement if isinstance(statement, _STATEMENT_NODES) else statement.node
61
+ context = SQLiteCompilationContext(self.capabilities)
62
+ if isinstance(node, CallNode):
63
+ message = "SQLite keeps no stored procedures; run the statements themselves"
64
+ raise UnsupportedQueryError(message)
65
+ if isinstance(node, (InsertNode, UpdateNode, DeleteNode)):
66
+ sql, statement_kind = SQLiteWriteCompilation(context).compile(node)
67
+ return context.result(sql, statement_kind, node)
68
+ sql = _SQLiteQueryCompilation(context).compile(node)
69
+ return context.result(sql, StatementKind.SELECT, node)
70
+
71
+
72
+ class _SQLiteQueryCompilation(QueryRenderer):
73
+ __slots__ = ("_context",)
74
+
75
+ def __init__(self, context: SQLiteCompilationContext) -> None:
76
+ self._context = context
77
+ super().__init__(SQLiteExpressionCompiler(context))
78
+
79
+ @property
80
+ def dialect(self) -> str:
81
+ return "SQLite"
82
+
83
+ @property
84
+ def quoting(self) -> IdentifierQuoting:
85
+ return QUOTING
86
+
87
+ def require_join_capability(self, kind: JoinKind) -> None:
88
+ if kind is JoinKind.RIGHT and not self._context.capabilities.right_join:
89
+ message = "SQLite RIGHT JOIN is disabled by compiler capabilities"
90
+ raise UnsupportedQueryError(message)
91
+ if kind is JoinKind.FULL and not self._context.capabilities.full_join:
92
+ message = "SQLite FULL JOIN is disabled by compiler capabilities"
93
+ raise UnsupportedQueryError(message)
94
+
95
+ def locking(self, node: LockNode) -> str:
96
+ """SQLite locks a database rather than rows, so it cannot do this."""
97
+ del node
98
+ message = (
99
+ "SQLite locks the whole database rather than rows, so it cannot "
100
+ "hold the rows a query read; use a transaction instead"
101
+ )
102
+ raise UnsupportedQueryError(message)
103
+
104
+ def null_ordered(
105
+ self,
106
+ expression: str,
107
+ direction: str,
108
+ nulls: NullOrder,
109
+ ) -> str:
110
+ if not self._context.capabilities.null_ordering:
111
+ message = "SQLite null ordering is disabled by compiler capabilities"
112
+ raise UnsupportedQueryError(message)
113
+ return super().null_ordered(expression, direction, nulls)
114
+
115
+ def require_recursive_common_tables(self) -> None:
116
+ if not self._context.capabilities.recursive_common_tables:
117
+ message = "SQLite recursive common tables are disabled"
118
+ raise UnsupportedQueryError(message)
119
+
120
+ def require_lateral_sources(self) -> None:
121
+ if not self._context.capabilities.lateral_sources:
122
+ message = "SQLite has no lateral sources"
123
+ raise UnsupportedQueryError(message)
124
+
125
+ def append_pagination(
126
+ self,
127
+ fragments: list[str],
128
+ limit: int | None,
129
+ offset: int | None,
130
+ ) -> None:
131
+ if limit is not None:
132
+ fragments.append(f"LIMIT {self._context.bind_page_value(limit)}")
133
+ elif offset is not None:
134
+ fragments.append("LIMIT -1")
135
+ if offset is not None:
136
+ fragments.append(f"OFFSET {self._context.bind_page_value(offset)}")
137
+
138
+
139
+ __all__ = ("QueryProvider", "SQLiteCompiler")
@@ -0,0 +1,20 @@
1
+ """SQLite compilation context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.execution import CompilationContext
6
+ from pyoq.query.sqlite.model import SQLiteCapabilities
7
+
8
+
9
+ class SQLiteCompilationContext(CompilationContext):
10
+ __slots__ = ("capabilities",)
11
+
12
+ def __init__(self, capabilities: SQLiteCapabilities) -> None:
13
+ super().__init__(
14
+ capabilities.maximum_parameters,
15
+ capabilities.parameter_style,
16
+ )
17
+ self.capabilities = capabilities
18
+
19
+
20
+ __all__ = ("SQLiteCompilationContext",)
@@ -0,0 +1,119 @@
1
+ """Synchronous typed SQLite query execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from collections.abc import Callable
7
+ from typing import TypeVar
8
+
9
+ from pyoq.query.execution import ExecutionControl
10
+ from pyoq.query.select import Query
11
+ from pyoq.query.sqlite.commands import open_cursor, run_sql
12
+ from pyoq.query.sqlite.compiler import SQLiteCompiler
13
+ from pyoq.query.sqlite.model import CompiledQuery
14
+ from pyoq.query.sqlite.operations import SQLiteQueryOperations
15
+ from pyoq.query.sqlite.parameters import adapt_parameters
16
+ from pyoq.query.sqlite.pool import SQLitePool
17
+ from pyoq.query.sqlite.streaming import (
18
+ SQLiteRowStream,
19
+ SQLiteStreamPolicy,
20
+ SQLiteStreamResource,
21
+ )
22
+ from pyoq.query.sqlite.transactions import SQLiteTransaction, TransactionMode
23
+
24
+ OperationResult = TypeVar("OperationResult")
25
+ ResultRow = TypeVar("ResultRow")
26
+
27
+
28
+ class SQLiteExecutor(SQLiteQueryOperations):
29
+ __slots__ = ("_pool",)
30
+
31
+ def __init__(
32
+ self,
33
+ pool: SQLitePool,
34
+ compiler: SQLiteCompiler | None = None,
35
+ ) -> None:
36
+ self._pool = pool
37
+ super().__init__(compiler or SQLiteCompiler())
38
+
39
+ @property
40
+ def pool(self) -> SQLitePool:
41
+ return self._pool
42
+
43
+ def stream(
44
+ self,
45
+ query: Query[ResultRow],
46
+ /,
47
+ *,
48
+ policy: SQLiteStreamPolicy | None = None,
49
+ ) -> SQLiteRowStream[ResultRow]:
50
+ stream_policy = policy or SQLiteStreamPolicy()
51
+ statement = self.compiler.compile(query)
52
+ return SQLiteRowStream(
53
+ lambda: self._open_stream(
54
+ statement.sql,
55
+ statement.parameters,
56
+ stream_policy,
57
+ ),
58
+ stream_policy,
59
+ statement.row_types,
60
+ )
61
+
62
+ def transaction(
63
+ self,
64
+ mode: TransactionMode = TransactionMode.DEFERRED,
65
+ *,
66
+ control: ExecutionControl | None = None,
67
+ ) -> SQLiteTransaction:
68
+ return SQLiteTransaction(
69
+ self.pool,
70
+ self.compiler,
71
+ mode,
72
+ control,
73
+ )
74
+
75
+ def _run(
76
+ self,
77
+ statement: CompiledQuery,
78
+ operation: Callable[[sqlite3.Cursor], OperationResult],
79
+ control: ExecutionControl | None,
80
+ ) -> OperationResult:
81
+ parameters = adapt_parameters(statement.parameters)
82
+ lease = self.pool.checkout()
83
+ with lease as connection:
84
+ return run_sql(
85
+ connection,
86
+ statement.sql,
87
+ parameters,
88
+ operation,
89
+ control,
90
+ lease.invalidate,
91
+ )
92
+
93
+ def _open_stream(
94
+ self,
95
+ sql: str,
96
+ parameters: tuple[object, ...],
97
+ policy: SQLiteStreamPolicy,
98
+ ) -> SQLiteStreamResource:
99
+ adapted_parameters = adapt_parameters(parameters)
100
+ lease = self.pool.checkout()
101
+ connection = lease.__enter__()
102
+ try:
103
+ session = open_cursor(
104
+ connection,
105
+ sql,
106
+ adapted_parameters,
107
+ policy.control,
108
+ lease.invalidate,
109
+ )
110
+ except BaseException:
111
+ lease.__exit__(None, None, None)
112
+ raise
113
+ return SQLiteStreamResource(
114
+ session,
115
+ lambda: lease.__exit__(None, None, None),
116
+ )
117
+
118
+
119
+ __all__ = ("SQLiteExecutor",)
@@ -0,0 +1,224 @@
1
+ """SQLite scalar expression compilation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from pyoq.config import DatabaseDialect
8
+ from pyoq.errors import UnsupportedQueryError
9
+ from pyoq.query.nodes import (
10
+ BinaryNode,
11
+ BinaryOperator,
12
+ DatePart,
13
+ ExtractNode,
14
+ FieldNode,
15
+ FunctionName,
16
+ FunctionNode,
17
+ JsonContainsNode,
18
+ JsonKeysNode,
19
+ JsonNode,
20
+ JsonOperation,
21
+ JsonWrite,
22
+ JsonWriteNode,
23
+ ScalarFamily,
24
+ )
25
+ from pyoq.query.rendering.expressions import (
26
+ CollectionFunctions,
27
+ ExpressionRenderer,
28
+ )
29
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
30
+ from pyoq.query.results import expression_family
31
+ from pyoq.query.select_nodes import JsonElementsSourceNode
32
+ from pyoq.query.sqlite.context import SQLiteCompilationContext
33
+ from pyoq.query.sqlite.identifiers import QUOTING
34
+ from pyoq.schema.models import TypeKind
35
+
36
+ _SQLITE_BINARY_SQL = {
37
+ BinaryOperator.IS_DISTINCT_FROM: "IS NOT",
38
+ BinaryOperator.IS_NOT_DISTINCT_FROM: "IS",
39
+ }
40
+
41
+ _DATE_FORMAT = {
42
+ DatePart.YEAR: "%Y",
43
+ DatePart.MONTH: "%m",
44
+ DatePart.DAY: "%d",
45
+ DatePart.HOUR: "%H",
46
+ DatePart.MINUTE: "%M",
47
+ DatePart.SECOND: "%S",
48
+ }
49
+
50
+
51
+ _SQLITE_CAST_SQL: Mapping[TypeKind, str] = {
52
+ TypeKind.BOOLEAN: "INTEGER",
53
+ TypeKind.SMALL_INTEGER: "INTEGER",
54
+ TypeKind.INTEGER: "INTEGER",
55
+ TypeKind.BIG_INTEGER: "INTEGER",
56
+ TypeKind.DECIMAL: "NUMERIC",
57
+ TypeKind.REAL: "REAL",
58
+ TypeKind.DOUBLE: "REAL",
59
+ TypeKind.STRING: "TEXT",
60
+ TypeKind.BINARY: "BLOB",
61
+ }
62
+ """The five storage classes SQLite has, and nothing it would only tolerate.
63
+
64
+ SQLite accepts `CAST(x AS DATE)` and answers with an integer, because an
65
+ unknown type name falls back to affinity rather than being rejected. Naming a
66
+ type it does not have would promise a date and return something else, so the
67
+ kinds it cannot do are absent and refused.
68
+ """
69
+
70
+ _SQLITE_CHOOSING_SQL: Mapping[FunctionName, str] = {
71
+ FunctionName.GREATEST: "MAX",
72
+ FunctionName.LEAST: "MIN",
73
+ }
74
+
75
+
76
+ _SQLITE_JSON_WRITES: Mapping[JsonWrite, str] = {
77
+ JsonWrite.SET: "JSON_SET",
78
+ JsonWrite.INSERT: "JSON_INSERT",
79
+ JsonWrite.REPLACE: "JSON_REPLACE",
80
+ }
81
+
82
+
83
+ class SQLiteExpressionCompiler(ExpressionRenderer[SQLiteCompilationContext]):
84
+ __slots__ = ()
85
+
86
+ @property
87
+ def collection_functions(self) -> CollectionFunctions:
88
+ return CollectionFunctions(
89
+ "json_group_array",
90
+ "json_object",
91
+ "json_array()",
92
+ "json_array",
93
+ "json_group_object",
94
+ ordered=True,
95
+ )
96
+
97
+ @property
98
+ def dialect(self) -> str:
99
+ return "SQLite"
100
+
101
+ def qualified_field(self, node: FieldNode) -> str:
102
+ return QUOTING.qualified(
103
+ node.name,
104
+ table=node.table,
105
+ schema=node.schema,
106
+ catalog=node.catalog,
107
+ )
108
+
109
+ def binary_operator_sql(self, operator: BinaryOperator) -> str | None:
110
+ override = _SQLITE_BINARY_SQL.get(operator)
111
+ if override is not None:
112
+ return override
113
+ return super().binary_operator_sql(operator)
114
+
115
+ def validate_binary(self, node: BinaryNode) -> None:
116
+ if node.operator not in {BinaryOperator.ADD, BinaryOperator.SUBTRACT}:
117
+ return
118
+ families = {expression_family(node.left), expression_family(node.right)}
119
+ if ScalarFamily.TEMPORAL in families:
120
+ message = "SQLite temporal arithmetic requires an explicit expression"
121
+ raise UnsupportedQueryError(message)
122
+
123
+ def render_json_elements(self, node: JsonElementsSourceNode) -> str:
124
+ """SQLite walks a JSON value and already calls the column it yields value."""
125
+ subject = self.compile(node.expression)
126
+ return f"JSON_EACH({subject}) AS {QUOTING.quote(node.alias)}"
127
+
128
+ def render_json_keys(self, node: JsonKeysNode) -> str:
129
+ """SQLite walks the members of an object one row at a time."""
130
+ subject = self.compile(node.expression)
131
+ return f"(SELECT JSON_GROUP_ARRAY(key) FROM JSON_EACH({subject}))" # noqa: S608
132
+
133
+ def render_json(self, node: JsonNode) -> str:
134
+ """SQLite walks a written path."""
135
+ subject = self.compile(node.expression)
136
+ path = self.json_path_text(node.path)
137
+ if node.operation is JsonOperation.REMOVE:
138
+ return f"JSON_REMOVE({subject}, {path})"
139
+ if node.operation is JsonOperation.EXISTS:
140
+ return f"(JSON_TYPE({subject}, {path}) IS NOT NULL)"
141
+ if node.operation is JsonOperation.LENGTH:
142
+ return f"JSON_ARRAY_LENGTH({subject}, {path})"
143
+ if node.operation is JsonOperation.TEXT:
144
+ return f"JSON_EXTRACT({subject}, {path})"
145
+ return f"({subject} -> {path})"
146
+
147
+ def render_json_write(self, node: JsonWriteNode) -> str:
148
+ """Compiled in the order it is written, because binding follows that."""
149
+ subject = self.compile(node.expression)
150
+ if node.operation is JsonWrite.MERGE:
151
+ return f"JSON_PATCH({subject}, {self.compile(node.value)})"
152
+ if node.operation is JsonWrite.CONCAT:
153
+ raise UnsupportedQueryError(_NO_CONCAT)
154
+ path = self.json_path_text(node.path)
155
+ value = self.compile(node.value)
156
+ return f"{_SQLITE_JSON_WRITES[node.operation]}({subject}, {path}, {value})"
157
+
158
+ def render_json_contains(self, node: JsonContainsNode) -> str:
159
+ """SQLite has no containment operator, and will not pretend to."""
160
+ del node
161
+ message = "SQLite has no JSON containment; compare the members you mean instead"
162
+ raise UnsupportedQueryError(message)
163
+
164
+ @property
165
+ def quoting(self) -> IdentifierQuoting:
166
+ return QUOTING
167
+
168
+ @property
169
+ def database_dialect(self) -> DatabaseDialect:
170
+ return DatabaseDialect.SQLITE
171
+
172
+ def render_extract(self, node: ExtractNode) -> str:
173
+ date_format = _DATE_FORMAT.get(node.part)
174
+ if date_format is None:
175
+ message = f"unsupported SQLite date part: {node.part}"
176
+ raise UnsupportedQueryError(message)
177
+ return (
178
+ f"CAST(STRFTIME('{date_format}', {self.compile(node.expression)}) "
179
+ "AS INTEGER)"
180
+ )
181
+
182
+ @property
183
+ def cast_type_names(self) -> Mapping[TypeKind, str]:
184
+ return _SQLITE_CAST_SQL
185
+
186
+ def require_window_functions(self) -> None:
187
+ """Window functions need SQLite 3.25 or later."""
188
+ if not self._context.capabilities.window_functions:
189
+ message = "this SQLite build cannot measure a row against its neighbours"
190
+ raise UnsupportedQueryError(message)
191
+
192
+ def choosing_function_sql(self, name: FunctionName) -> str:
193
+ """SQLite spells the widest and narrowest of several as MAX and MIN."""
194
+ return _SQLITE_CHOOSING_SQL.get(name, super().choosing_function_sql(name))
195
+
196
+ def render_string_predicate(self, node: FunctionNode) -> str:
197
+ left_node, right_node = node.arguments
198
+ left = self.compile(left_node)
199
+ right = self.compile(right_node)
200
+ if node.name is FunctionName.CONTAINS:
201
+ return f"(INSTR({left}, {right}) > 0)"
202
+ if node.name is FunctionName.STARTS_WITH:
203
+ comparison = self.compile(right_node)
204
+ return f"(SUBSTR({left}, 1, LENGTH({right})) = {comparison})"
205
+ if node.name is FunctionName.ENDS_WITH:
206
+ comparison = self.compile(right_node)
207
+ return f"(SUBSTR({left}, -LENGTH({right})) = {comparison})"
208
+ message = f"unsupported SQLite function: {node.name}"
209
+ raise UnsupportedQueryError(message)
210
+
211
+ def require_raw_expressions(self) -> None:
212
+ if not self._context.capabilities.raw_expressions:
213
+ message = "SQLite raw expressions are disabled by compiler capabilities"
214
+ raise UnsupportedQueryError(message)
215
+
216
+
217
+ __all__ = ("SQLiteExpressionCompiler", "expression_family")
218
+
219
+
220
+ _NO_CONCAT = (
221
+ "SQLite merges a document all the way down and has no one-level "
222
+ "concatenation; json_merge says what it does"
223
+ )
224
+ """Refused rather than emulated, because all the way down is not one level."""