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.
- pyoq/__init__.py +7 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +19 -0
- pyoq/config/loader.py +204 -0
- pyoq/config/models.py +243 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +72 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +62 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +19 -0
- pyoq/serving/databases.py +107 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.0.dist-info/METADATA +3034 -0
- pyoq_sql-1.0.0.dist-info/RECORD +264 -0
- pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
- 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."""
|