sqlspec 0.11.1__py3-none-any.whl → 0.12.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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +16 -3
- sqlspec/_serialization.py +3 -10
- sqlspec/_sql.py +1147 -0
- sqlspec/_typing.py +343 -41
- sqlspec/adapters/adbc/__init__.py +2 -6
- sqlspec/adapters/adbc/config.py +474 -149
- sqlspec/adapters/adbc/driver.py +330 -621
- sqlspec/adapters/aiosqlite/__init__.py +2 -6
- sqlspec/adapters/aiosqlite/config.py +143 -57
- sqlspec/adapters/aiosqlite/driver.py +269 -431
- sqlspec/adapters/asyncmy/__init__.py +3 -8
- sqlspec/adapters/asyncmy/config.py +247 -202
- sqlspec/adapters/asyncmy/driver.py +218 -436
- sqlspec/adapters/asyncpg/__init__.py +4 -7
- sqlspec/adapters/asyncpg/config.py +329 -176
- sqlspec/adapters/asyncpg/driver.py +417 -487
- sqlspec/adapters/bigquery/__init__.py +2 -2
- sqlspec/adapters/bigquery/config.py +407 -0
- sqlspec/adapters/bigquery/driver.py +600 -553
- sqlspec/adapters/duckdb/__init__.py +4 -1
- sqlspec/adapters/duckdb/config.py +432 -321
- sqlspec/adapters/duckdb/driver.py +392 -406
- sqlspec/adapters/oracledb/__init__.py +3 -8
- sqlspec/adapters/oracledb/config.py +625 -0
- sqlspec/adapters/oracledb/driver.py +548 -921
- sqlspec/adapters/psqlpy/__init__.py +4 -7
- sqlspec/adapters/psqlpy/config.py +372 -203
- sqlspec/adapters/psqlpy/driver.py +197 -533
- sqlspec/adapters/psycopg/__init__.py +3 -8
- sqlspec/adapters/psycopg/config.py +741 -0
- sqlspec/adapters/psycopg/driver.py +734 -694
- sqlspec/adapters/sqlite/__init__.py +2 -6
- sqlspec/adapters/sqlite/config.py +146 -81
- sqlspec/adapters/sqlite/driver.py +242 -405
- sqlspec/base.py +220 -784
- sqlspec/config.py +354 -0
- sqlspec/driver/__init__.py +22 -0
- sqlspec/driver/_async.py +252 -0
- sqlspec/driver/_common.py +338 -0
- sqlspec/driver/_sync.py +261 -0
- sqlspec/driver/mixins/__init__.py +17 -0
- sqlspec/driver/mixins/_pipeline.py +523 -0
- sqlspec/driver/mixins/_result_utils.py +122 -0
- sqlspec/driver/mixins/_sql_translator.py +35 -0
- sqlspec/driver/mixins/_storage.py +993 -0
- sqlspec/driver/mixins/_type_coercion.py +131 -0
- sqlspec/exceptions.py +299 -7
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +474 -0
- sqlspec/extensions/litestar/__init__.py +1 -6
- sqlspec/extensions/litestar/_utils.py +1 -5
- sqlspec/extensions/litestar/config.py +5 -6
- sqlspec/extensions/litestar/handlers.py +13 -12
- sqlspec/extensions/litestar/plugin.py +22 -24
- sqlspec/extensions/litestar/providers.py +37 -55
- sqlspec/loader.py +528 -0
- sqlspec/service/__init__.py +3 -0
- sqlspec/service/base.py +24 -0
- sqlspec/service/pagination.py +26 -0
- sqlspec/statement/__init__.py +21 -0
- sqlspec/statement/builder/__init__.py +54 -0
- sqlspec/statement/builder/_ddl_utils.py +119 -0
- sqlspec/statement/builder/_parsing_utils.py +135 -0
- sqlspec/statement/builder/base.py +328 -0
- sqlspec/statement/builder/ddl.py +1379 -0
- sqlspec/statement/builder/delete.py +80 -0
- sqlspec/statement/builder/insert.py +274 -0
- sqlspec/statement/builder/merge.py +95 -0
- sqlspec/statement/builder/mixins/__init__.py +65 -0
- sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
- sqlspec/statement/builder/mixins/_case_builder.py +91 -0
- sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
- sqlspec/statement/builder/mixins/_delete_from.py +34 -0
- sqlspec/statement/builder/mixins/_from.py +61 -0
- sqlspec/statement/builder/mixins/_group_by.py +119 -0
- sqlspec/statement/builder/mixins/_having.py +35 -0
- sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
- sqlspec/statement/builder/mixins/_insert_into.py +36 -0
- sqlspec/statement/builder/mixins/_insert_values.py +69 -0
- sqlspec/statement/builder/mixins/_join.py +110 -0
- sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
- sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
- sqlspec/statement/builder/mixins/_order_by.py +46 -0
- sqlspec/statement/builder/mixins/_pivot.py +82 -0
- sqlspec/statement/builder/mixins/_returning.py +37 -0
- sqlspec/statement/builder/mixins/_select_columns.py +60 -0
- sqlspec/statement/builder/mixins/_set_ops.py +122 -0
- sqlspec/statement/builder/mixins/_unpivot.py +80 -0
- sqlspec/statement/builder/mixins/_update_from.py +54 -0
- sqlspec/statement/builder/mixins/_update_set.py +91 -0
- sqlspec/statement/builder/mixins/_update_table.py +29 -0
- sqlspec/statement/builder/mixins/_where.py +374 -0
- sqlspec/statement/builder/mixins/_window_functions.py +86 -0
- sqlspec/statement/builder/protocols.py +20 -0
- sqlspec/statement/builder/select.py +206 -0
- sqlspec/statement/builder/update.py +178 -0
- sqlspec/statement/filters.py +571 -0
- sqlspec/statement/parameters.py +736 -0
- sqlspec/statement/pipelines/__init__.py +67 -0
- sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
- sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
- sqlspec/statement/pipelines/base.py +315 -0
- sqlspec/statement/pipelines/context.py +119 -0
- sqlspec/statement/pipelines/result_types.py +41 -0
- sqlspec/statement/pipelines/transformers/__init__.py +8 -0
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
- sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
- sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
- sqlspec/statement/pipelines/validators/__init__.py +23 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
- sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
- sqlspec/statement/pipelines/validators/_performance.py +703 -0
- sqlspec/statement/pipelines/validators/_security.py +990 -0
- sqlspec/statement/pipelines/validators/base.py +67 -0
- sqlspec/statement/result.py +527 -0
- sqlspec/statement/splitter.py +701 -0
- sqlspec/statement/sql.py +1198 -0
- sqlspec/storage/__init__.py +15 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +166 -0
- sqlspec/storage/backends/fsspec.py +315 -0
- sqlspec/storage/backends/obstore.py +464 -0
- sqlspec/storage/protocol.py +170 -0
- sqlspec/storage/registry.py +315 -0
- sqlspec/typing.py +157 -36
- sqlspec/utils/correlation.py +155 -0
- sqlspec/utils/deprecation.py +3 -6
- sqlspec/utils/fixtures.py +6 -11
- sqlspec/utils/logging.py +135 -0
- sqlspec/utils/module_loader.py +45 -43
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +6 -8
- sqlspec/utils/sync_tools.py +15 -27
- sqlspec/utils/text.py +58 -26
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/METADATA +97 -26
- sqlspec-0.12.0.dist-info/RECORD +145 -0
- sqlspec/adapters/bigquery/config/__init__.py +0 -3
- sqlspec/adapters/bigquery/config/_common.py +0 -40
- sqlspec/adapters/bigquery/config/_sync.py +0 -87
- sqlspec/adapters/oracledb/config/__init__.py +0 -9
- sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
- sqlspec/adapters/oracledb/config/_common.py +0 -131
- sqlspec/adapters/oracledb/config/_sync.py +0 -186
- sqlspec/adapters/psycopg/config/__init__.py +0 -19
- sqlspec/adapters/psycopg/config/_async.py +0 -169
- sqlspec/adapters/psycopg/config/_common.py +0 -56
- sqlspec/adapters/psycopg/config/_sync.py +0 -168
- sqlspec/filters.py +0 -331
- sqlspec/mixins.py +0 -305
- sqlspec/statement.py +0 -378
- sqlspec-0.11.1.dist-info/RECORD +0 -69
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,456 +1,294 @@
|
|
|
1
|
+
import csv
|
|
1
2
|
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator, Sequence
|
|
2
4
|
from contextlib import asynccontextmanager
|
|
3
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
4
7
|
|
|
5
8
|
import aiosqlite
|
|
6
|
-
from sqlglot import exp
|
|
7
9
|
|
|
8
|
-
from sqlspec.
|
|
9
|
-
from sqlspec.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
from sqlspec.driver import AsyncDriverAdapterProtocol
|
|
11
|
+
from sqlspec.driver.mixins import (
|
|
12
|
+
AsyncPipelinedExecutionMixin,
|
|
13
|
+
AsyncStorageMixin,
|
|
14
|
+
SQLTranslatorMixin,
|
|
15
|
+
ToSchemaMixin,
|
|
16
|
+
TypeCoercionMixin,
|
|
17
|
+
)
|
|
18
|
+
from sqlspec.statement.parameters import ParameterStyle
|
|
19
|
+
from sqlspec.statement.result import DMLResultDict, ScriptResultDict, SelectResultDict, SQLResult
|
|
20
|
+
from sqlspec.statement.sql import SQL, SQLConfig
|
|
21
|
+
from sqlspec.typing import DictRow, ModelDTOT, RowT
|
|
22
|
+
from sqlspec.utils.serializers import to_json
|
|
13
23
|
|
|
14
24
|
if TYPE_CHECKING:
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
from sqlspec.typing import ModelDTOT, StatementParameterType, T
|
|
25
|
+
from sqlglot.dialects.dialect import DialectType
|
|
18
26
|
|
|
19
27
|
__all__ = ("AiosqliteConnection", "AiosqliteDriver")
|
|
20
|
-
AiosqliteConnection = aiosqlite.Connection
|
|
21
28
|
|
|
22
29
|
logger = logging.getLogger("sqlspec")
|
|
23
30
|
|
|
31
|
+
AiosqliteConnection = aiosqlite.Connection
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
class AiosqliteDriver(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
AsyncDriverAdapterProtocol[AiosqliteConnection, RowT],
|
|
36
|
+
SQLTranslatorMixin,
|
|
37
|
+
TypeCoercionMixin,
|
|
38
|
+
AsyncStorageMixin,
|
|
39
|
+
AsyncPipelinedExecutionMixin,
|
|
40
|
+
ToSchemaMixin,
|
|
29
41
|
):
|
|
30
|
-
"""SQLite
|
|
42
|
+
"""Aiosqlite SQLite Driver Adapter. Modern protocol implementation."""
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
dialect: "DialectType" = "sqlite"
|
|
45
|
+
supported_parameter_styles: "tuple[ParameterStyle, ...]" = (ParameterStyle.QMARK, ParameterStyle.NAMED_COLON)
|
|
46
|
+
default_parameter_style: ParameterStyle = ParameterStyle.QMARK
|
|
47
|
+
__slots__ = ()
|
|
34
48
|
|
|
35
|
-
def __init__(
|
|
36
|
-
self
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
connection: AiosqliteConnection,
|
|
52
|
+
config: "Optional[SQLConfig]" = None,
|
|
53
|
+
default_row_type: "type[DictRow]" = DictRow,
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(connection=connection, config=config, default_row_type=default_row_type)
|
|
56
|
+
|
|
57
|
+
# AIOSQLite-specific type coercion overrides (same as SQLite)
|
|
58
|
+
def _coerce_boolean(self, value: Any) -> Any:
|
|
59
|
+
"""AIOSQLite/SQLite stores booleans as integers (0/1)."""
|
|
60
|
+
if isinstance(value, bool):
|
|
61
|
+
return 1 if value else 0
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def _coerce_decimal(self, value: Any) -> Any:
|
|
65
|
+
"""AIOSQLite/SQLite stores decimals as strings to preserve precision."""
|
|
66
|
+
if isinstance(value, str):
|
|
67
|
+
return value # Already a string
|
|
68
|
+
from decimal import Decimal
|
|
69
|
+
|
|
70
|
+
if isinstance(value, Decimal):
|
|
71
|
+
return str(value)
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
def _coerce_json(self, value: Any) -> Any:
|
|
75
|
+
"""AIOSQLite/SQLite stores JSON as strings (requires JSON1 extension)."""
|
|
76
|
+
if isinstance(value, (dict, list)):
|
|
77
|
+
return to_json(value)
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
def _coerce_array(self, value: Any) -> Any:
|
|
81
|
+
"""AIOSQLite/SQLite doesn't have native arrays - store as JSON strings."""
|
|
82
|
+
if isinstance(value, (list, tuple)):
|
|
83
|
+
return to_json(list(value))
|
|
84
|
+
return value
|
|
41
85
|
|
|
42
86
|
@asynccontextmanager
|
|
43
|
-
async def
|
|
44
|
-
|
|
87
|
+
async def _get_cursor(
|
|
88
|
+
self, connection: Optional[AiosqliteConnection] = None
|
|
89
|
+
) -> AsyncGenerator[aiosqlite.Cursor, None]:
|
|
90
|
+
conn_to_use = connection or self.connection
|
|
91
|
+
conn_to_use.row_factory = aiosqlite.Row
|
|
92
|
+
cursor = await conn_to_use.cursor()
|
|
45
93
|
try:
|
|
46
94
|
yield cursor
|
|
47
95
|
finally:
|
|
48
96
|
await cursor.close()
|
|
49
97
|
|
|
50
|
-
def
|
|
51
|
-
self,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
**kwargs: Any
|
|
147
|
-
) ->
|
|
148
|
-
|
|
149
|
-
self
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
async def
|
|
185
|
-
self,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
**kwargs: Any,
|
|
247
|
-
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
248
|
-
"""Fetch one row from the database.
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
The first row of the query results.
|
|
252
|
-
"""
|
|
253
|
-
connection = self._connection(connection)
|
|
254
|
-
sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
|
|
255
|
-
|
|
256
|
-
async with self._with_cursor(connection) as cursor:
|
|
257
|
-
await cursor.execute(sql, parameters or ())
|
|
258
|
-
result = await cursor.fetchone()
|
|
259
|
-
if result is None:
|
|
260
|
-
return None
|
|
261
|
-
column_names = [column[0] for column in cursor.description]
|
|
262
|
-
return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
|
|
263
|
-
|
|
264
|
-
@overload
|
|
265
|
-
async def select_value(
|
|
266
|
-
self,
|
|
267
|
-
sql: str,
|
|
268
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
269
|
-
*filters: "StatementFilter",
|
|
270
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
271
|
-
schema_type: None = None,
|
|
272
|
-
**kwargs: Any,
|
|
273
|
-
) -> "Any": ...
|
|
274
|
-
@overload
|
|
275
|
-
async def select_value(
|
|
276
|
-
self,
|
|
277
|
-
sql: str,
|
|
278
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
279
|
-
*filters: "StatementFilter",
|
|
280
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
281
|
-
schema_type: "type[T]",
|
|
282
|
-
**kwargs: Any,
|
|
283
|
-
) -> "T": ...
|
|
284
|
-
async def select_value(
|
|
285
|
-
self,
|
|
286
|
-
sql: str,
|
|
287
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
288
|
-
*filters: "StatementFilter",
|
|
289
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
290
|
-
schema_type: "Optional[type[T]]" = None,
|
|
291
|
-
**kwargs: Any,
|
|
292
|
-
) -> "Union[T, Any]":
|
|
293
|
-
"""Fetch a single value from the database.
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
The first value from the first row of results.
|
|
297
|
-
"""
|
|
298
|
-
connection = self._connection(connection)
|
|
299
|
-
sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
|
|
300
|
-
|
|
301
|
-
async with self._with_cursor(connection) as cursor:
|
|
302
|
-
await cursor.execute(sql, parameters or ())
|
|
303
|
-
result = await cursor.fetchone()
|
|
304
|
-
result = self.check_not_found(result)
|
|
305
|
-
|
|
306
|
-
# Return first value from the row
|
|
307
|
-
result_value = result[0]
|
|
308
|
-
if schema_type is None:
|
|
309
|
-
return result_value
|
|
310
|
-
return schema_type(result_value) # type: ignore[call-arg]
|
|
311
|
-
|
|
312
|
-
@overload
|
|
313
|
-
async def select_value_or_none(
|
|
314
|
-
self,
|
|
315
|
-
sql: str,
|
|
316
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
317
|
-
*filters: "StatementFilter",
|
|
318
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
319
|
-
schema_type: None = None,
|
|
320
|
-
**kwargs: Any,
|
|
321
|
-
) -> "Optional[Any]": ...
|
|
322
|
-
@overload
|
|
323
|
-
async def select_value_or_none(
|
|
324
|
-
self,
|
|
325
|
-
sql: str,
|
|
326
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
327
|
-
*filters: "StatementFilter",
|
|
328
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
329
|
-
schema_type: "type[T]",
|
|
330
|
-
**kwargs: Any,
|
|
331
|
-
) -> "Optional[T]": ...
|
|
332
|
-
async def select_value_or_none(
|
|
333
|
-
self,
|
|
334
|
-
sql: str,
|
|
335
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
336
|
-
*filters: "StatementFilter",
|
|
337
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
338
|
-
schema_type: "Optional[type[T]]" = None,
|
|
339
|
-
**kwargs: Any,
|
|
340
|
-
) -> "Optional[Union[T, Any]]":
|
|
341
|
-
"""Fetch a single value from the database.
|
|
342
|
-
|
|
343
|
-
Returns:
|
|
344
|
-
The first value from the first row of results, or None if no results.
|
|
345
|
-
"""
|
|
346
|
-
connection = self._connection(connection)
|
|
347
|
-
sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
|
|
348
|
-
|
|
349
|
-
async with self._with_cursor(connection) as cursor:
|
|
350
|
-
# Execute the query
|
|
351
|
-
await cursor.execute(sql, parameters or ())
|
|
352
|
-
result = await cursor.fetchone()
|
|
353
|
-
if result is None:
|
|
354
|
-
return None
|
|
355
|
-
result_value = result[0]
|
|
356
|
-
if schema_type is None:
|
|
357
|
-
return result_value
|
|
358
|
-
return schema_type(result_value) # type: ignore[call-arg]
|
|
359
|
-
|
|
360
|
-
async def insert_update_delete(
|
|
361
|
-
self,
|
|
362
|
-
sql: str,
|
|
363
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
364
|
-
*filters: "StatementFilter",
|
|
365
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
366
|
-
**kwargs: Any,
|
|
367
|
-
) -> int:
|
|
368
|
-
"""Insert, update, or delete data from the database.
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
Row count affected by the operation.
|
|
372
|
-
"""
|
|
373
|
-
connection = self._connection(connection)
|
|
374
|
-
sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
|
|
375
|
-
async with self._with_cursor(connection) as cursor:
|
|
376
|
-
# Execute the query
|
|
377
|
-
await cursor.execute(sql, parameters or ())
|
|
378
|
-
return cursor.rowcount
|
|
379
|
-
|
|
380
|
-
@overload
|
|
381
|
-
async def insert_update_delete_returning(
|
|
382
|
-
self,
|
|
383
|
-
sql: str,
|
|
384
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
385
|
-
*filters: "StatementFilter",
|
|
386
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
387
|
-
schema_type: None = None,
|
|
388
|
-
**kwargs: Any,
|
|
389
|
-
) -> "dict[str, Any]": ...
|
|
390
|
-
@overload
|
|
391
|
-
async def insert_update_delete_returning(
|
|
392
|
-
self,
|
|
393
|
-
sql: str,
|
|
394
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
395
|
-
*filters: "StatementFilter",
|
|
396
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
397
|
-
schema_type: "type[ModelDTOT]",
|
|
398
|
-
**kwargs: Any,
|
|
399
|
-
) -> "ModelDTOT": ...
|
|
400
|
-
async def insert_update_delete_returning(
|
|
401
|
-
self,
|
|
402
|
-
sql: str,
|
|
403
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
404
|
-
*filters: "StatementFilter",
|
|
405
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
406
|
-
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
407
|
-
**kwargs: Any,
|
|
408
|
-
) -> "Union[dict[str, Any], ModelDTOT]":
|
|
409
|
-
"""Insert, update, or delete data from the database and return result.
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
The first row of results.
|
|
413
|
-
"""
|
|
414
|
-
connection = self._connection(connection)
|
|
415
|
-
sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
|
|
416
|
-
|
|
417
|
-
async with self._with_cursor(connection) as cursor:
|
|
418
|
-
# Execute the query
|
|
419
|
-
await cursor.execute(sql, parameters or ())
|
|
420
|
-
result = await cursor.fetchone()
|
|
421
|
-
result = self.check_not_found(result)
|
|
422
|
-
column_names = [column[0] for column in cursor.description]
|
|
423
|
-
return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
|
|
424
|
-
|
|
425
|
-
async def execute_script(
|
|
426
|
-
self,
|
|
427
|
-
sql: str,
|
|
428
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
429
|
-
connection: "Optional[AiosqliteConnection]" = None,
|
|
430
|
-
**kwargs: Any,
|
|
431
|
-
) -> str:
|
|
432
|
-
"""Execute a script.
|
|
433
|
-
|
|
434
|
-
Returns:
|
|
435
|
-
Status message for the operation.
|
|
436
|
-
"""
|
|
437
|
-
connection = self._connection(connection)
|
|
438
|
-
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
439
|
-
|
|
440
|
-
async with self._with_cursor(connection) as cursor:
|
|
441
|
-
if parameters:
|
|
442
|
-
await cursor.execute(sql, parameters)
|
|
443
|
-
else:
|
|
444
|
-
await cursor.executescript(sql)
|
|
445
|
-
return "DONE"
|
|
446
|
-
|
|
447
|
-
def _connection(self, connection: "Optional[AiosqliteConnection]" = None) -> "AiosqliteConnection":
|
|
448
|
-
"""Get the connection to use for the operation.
|
|
449
|
-
|
|
450
|
-
Args:
|
|
451
|
-
connection: Optional connection to use.
|
|
452
|
-
|
|
453
|
-
Returns:
|
|
454
|
-
The connection to use.
|
|
455
|
-
"""
|
|
98
|
+
async def _execute_statement(
|
|
99
|
+
self, statement: SQL, connection: Optional[AiosqliteConnection] = None, **kwargs: Any
|
|
100
|
+
) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
|
|
101
|
+
if statement.is_script:
|
|
102
|
+
sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
|
|
103
|
+
return await self._execute_script(sql, connection=connection, **kwargs)
|
|
104
|
+
|
|
105
|
+
# Determine if we need to convert parameter style
|
|
106
|
+
detected_styles = {p.style for p in statement.parameter_info}
|
|
107
|
+
target_style = self.default_parameter_style
|
|
108
|
+
|
|
109
|
+
# Check if any detected style is not supported
|
|
110
|
+
unsupported_styles = detected_styles - set(self.supported_parameter_styles)
|
|
111
|
+
if unsupported_styles:
|
|
112
|
+
# Convert to default style if we have unsupported styles
|
|
113
|
+
target_style = self.default_parameter_style
|
|
114
|
+
elif detected_styles:
|
|
115
|
+
# Use the first detected style if all are supported
|
|
116
|
+
# Prefer the first supported style found
|
|
117
|
+
for style in detected_styles:
|
|
118
|
+
if style in self.supported_parameter_styles:
|
|
119
|
+
target_style = style
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if statement.is_many:
|
|
123
|
+
sql, params = statement.compile(placeholder_style=target_style)
|
|
124
|
+
|
|
125
|
+
# Process parameter list through type coercion
|
|
126
|
+
params = self._process_parameters(params)
|
|
127
|
+
|
|
128
|
+
return await self._execute_many(sql, params, connection=connection, **kwargs)
|
|
129
|
+
|
|
130
|
+
sql, params = statement.compile(placeholder_style=target_style)
|
|
131
|
+
|
|
132
|
+
# Process parameters through type coercion
|
|
133
|
+
params = self._process_parameters(params)
|
|
134
|
+
|
|
135
|
+
return await self._execute(sql, params, statement, connection=connection, **kwargs)
|
|
136
|
+
|
|
137
|
+
async def _execute(
|
|
138
|
+
self, sql: str, parameters: Any, statement: SQL, connection: Optional[AiosqliteConnection] = None, **kwargs: Any
|
|
139
|
+
) -> Union[SelectResultDict, DMLResultDict]:
|
|
140
|
+
conn = self._connection(connection)
|
|
141
|
+
# Convert parameters to the format expected by the SQL
|
|
142
|
+
# Note: SQL was already rendered with appropriate placeholder style in _execute_statement
|
|
143
|
+
if ":param_" in sql or (parameters and isinstance(parameters, dict)):
|
|
144
|
+
# SQL has named placeholders, ensure params are dict
|
|
145
|
+
converted_params = self._convert_parameters_to_driver_format(
|
|
146
|
+
sql, parameters, target_style=ParameterStyle.NAMED_COLON
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
# SQL has positional placeholders, ensure params are list/tuple
|
|
150
|
+
converted_params = self._convert_parameters_to_driver_format(
|
|
151
|
+
sql, parameters, target_style=ParameterStyle.QMARK
|
|
152
|
+
)
|
|
153
|
+
async with self._get_cursor(conn) as cursor:
|
|
154
|
+
# Aiosqlite handles both dict and tuple parameters
|
|
155
|
+
await cursor.execute(sql, converted_params or ())
|
|
156
|
+
if self.returns_rows(statement.expression):
|
|
157
|
+
fetched_data = await cursor.fetchall()
|
|
158
|
+
column_names = [desc[0] for desc in cursor.description or []]
|
|
159
|
+
# Convert to list of dicts or tuples as expected by TypedDict
|
|
160
|
+
data_list: list[Any] = list(fetched_data) if fetched_data else []
|
|
161
|
+
result: SelectResultDict = {
|
|
162
|
+
"data": data_list,
|
|
163
|
+
"column_names": column_names,
|
|
164
|
+
"rows_affected": len(data_list),
|
|
165
|
+
}
|
|
166
|
+
return result
|
|
167
|
+
dml_result: DMLResultDict = {"rows_affected": cursor.rowcount, "status_message": "OK"}
|
|
168
|
+
return dml_result
|
|
169
|
+
|
|
170
|
+
async def _execute_many(
|
|
171
|
+
self, sql: str, param_list: Any, connection: Optional[AiosqliteConnection] = None, **kwargs: Any
|
|
172
|
+
) -> DMLResultDict:
|
|
173
|
+
conn = self._connection(connection)
|
|
174
|
+
logger.debug("Executing SQL (executemany): %s", sql)
|
|
175
|
+
if param_list:
|
|
176
|
+
logger.debug("Query parameters (batch): %s", param_list)
|
|
177
|
+
|
|
178
|
+
# Convert parameter list to proper format for executemany
|
|
179
|
+
params_list: list[tuple[Any, ...]] = []
|
|
180
|
+
if param_list and isinstance(param_list, Sequence):
|
|
181
|
+
for param_set in param_list:
|
|
182
|
+
param_set = cast("Any", param_set)
|
|
183
|
+
if isinstance(param_set, (list, tuple)):
|
|
184
|
+
params_list.append(tuple(param_set))
|
|
185
|
+
elif param_set is None:
|
|
186
|
+
params_list.append(())
|
|
187
|
+
|
|
188
|
+
async with self._get_cursor(conn) as cursor:
|
|
189
|
+
await cursor.executemany(sql, params_list)
|
|
190
|
+
result: DMLResultDict = {"rows_affected": cursor.rowcount, "status_message": "OK"}
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
async def _execute_script(
|
|
194
|
+
self, script: str, connection: Optional[AiosqliteConnection] = None, **kwargs: Any
|
|
195
|
+
) -> ScriptResultDict:
|
|
196
|
+
conn = self._connection(connection)
|
|
197
|
+
async with self._get_cursor(conn) as cursor:
|
|
198
|
+
await cursor.executescript(script)
|
|
199
|
+
result: ScriptResultDict = {
|
|
200
|
+
"statements_executed": -1, # AIOSQLite doesn't provide this info
|
|
201
|
+
"status_message": "SCRIPT EXECUTED",
|
|
202
|
+
}
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
async def _bulk_load_file(self, file_path: Path, table_name: str, format: str, mode: str, **options: Any) -> int:
|
|
206
|
+
"""Database-specific bulk load implementation."""
|
|
207
|
+
# TODO: convert this to use the storage backend. it has async support
|
|
208
|
+
if format != "csv":
|
|
209
|
+
msg = f"aiosqlite driver only supports CSV for bulk loading, not {format}."
|
|
210
|
+
raise NotImplementedError(msg)
|
|
211
|
+
|
|
212
|
+
conn = await self._create_connection() # type: ignore[attr-defined]
|
|
213
|
+
try:
|
|
214
|
+
async with self._get_cursor(conn) as cursor:
|
|
215
|
+
if mode == "replace":
|
|
216
|
+
await cursor.execute(f"DELETE FROM {table_name}")
|
|
217
|
+
|
|
218
|
+
# Using sync file IO here as it's a fallback path and aiofiles is not a dependency
|
|
219
|
+
with Path(file_path).open(encoding="utf-8") as f: # noqa: ASYNC230
|
|
220
|
+
reader = csv.reader(f, **options)
|
|
221
|
+
header = next(reader) # Skip header
|
|
222
|
+
placeholders = ", ".join("?" for _ in header)
|
|
223
|
+
sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
|
|
224
|
+
data_iter = list(reader)
|
|
225
|
+
await cursor.executemany(sql, data_iter)
|
|
226
|
+
rowcount = cursor.rowcount
|
|
227
|
+
await conn.commit()
|
|
228
|
+
return rowcount
|
|
229
|
+
finally:
|
|
230
|
+
await conn.close()
|
|
231
|
+
|
|
232
|
+
async def _wrap_select_result(
|
|
233
|
+
self, statement: SQL, result: SelectResultDict, schema_type: "Optional[type[ModelDTOT]]" = None, **kwargs: Any
|
|
234
|
+
) -> Union[SQLResult[ModelDTOT], SQLResult[RowT]]:
|
|
235
|
+
fetched_data = result["data"]
|
|
236
|
+
column_names = result["column_names"]
|
|
237
|
+
rows_affected = result["rows_affected"]
|
|
238
|
+
|
|
239
|
+
rows_as_dicts: list[dict[str, Any]] = [dict(row) for row in fetched_data]
|
|
240
|
+
|
|
241
|
+
if self.returns_rows(statement.expression):
|
|
242
|
+
converted_data_seq = self.to_schema(data=rows_as_dicts, schema_type=schema_type)
|
|
243
|
+
return SQLResult[ModelDTOT](
|
|
244
|
+
statement=statement,
|
|
245
|
+
data=list(converted_data_seq),
|
|
246
|
+
column_names=column_names,
|
|
247
|
+
rows_affected=rows_affected,
|
|
248
|
+
operation_type="SELECT",
|
|
249
|
+
)
|
|
250
|
+
return SQLResult[RowT](
|
|
251
|
+
statement=statement,
|
|
252
|
+
data=rows_as_dicts,
|
|
253
|
+
column_names=column_names,
|
|
254
|
+
rows_affected=rows_affected,
|
|
255
|
+
operation_type="SELECT",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
async def _wrap_execute_result(
|
|
259
|
+
self, statement: SQL, result: Union[DMLResultDict, ScriptResultDict], **kwargs: Any
|
|
260
|
+
) -> SQLResult[RowT]:
|
|
261
|
+
operation_type = "UNKNOWN"
|
|
262
|
+
if statement.expression:
|
|
263
|
+
operation_type = str(statement.expression.key).upper()
|
|
264
|
+
|
|
265
|
+
if "statements_executed" in result:
|
|
266
|
+
script_result = cast("ScriptResultDict", result)
|
|
267
|
+
return SQLResult[RowT](
|
|
268
|
+
statement=statement,
|
|
269
|
+
data=[],
|
|
270
|
+
rows_affected=0,
|
|
271
|
+
operation_type="SCRIPT",
|
|
272
|
+
total_statements=script_result.get("statements_executed", -1),
|
|
273
|
+
metadata={"status_message": script_result.get("status_message", "")},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if "rows_affected" in result:
|
|
277
|
+
dml_result = cast("DMLResultDict", result)
|
|
278
|
+
rows_affected = dml_result["rows_affected"]
|
|
279
|
+
status_message = dml_result["status_message"]
|
|
280
|
+
return SQLResult[RowT](
|
|
281
|
+
statement=statement,
|
|
282
|
+
data=[],
|
|
283
|
+
rows_affected=rows_affected,
|
|
284
|
+
operation_type=operation_type,
|
|
285
|
+
metadata={"status_message": status_message},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# This shouldn't happen with TypedDict approach
|
|
289
|
+
msg = f"Unexpected result type: {type(result)}"
|
|
290
|
+
raise ValueError(msg)
|
|
291
|
+
|
|
292
|
+
def _connection(self, connection: Optional[AiosqliteConnection] = None) -> AiosqliteConnection:
|
|
293
|
+
"""Get the connection to use for the operation."""
|
|
456
294
|
return connection or self.connection
|