sqlspec 0.11.1__py3-none-any.whl → 0.12.1__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.

Files changed (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.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 typing import TYPE_CHECKING, Any, Optional, Union, overload
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.base import AsyncDriverAdapterProtocol
9
- from sqlspec.filters import StatementFilter
10
- from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
11
- from sqlspec.statement import SQLStatement
12
- from sqlspec.typing import is_dict
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 collections.abc import AsyncGenerator, Mapping, Sequence # Added Mapping, Sequence
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
- SQLTranslatorMixin["AiosqliteConnection"],
27
- AsyncDriverAdapterProtocol["AiosqliteConnection"],
28
- ResultConverter,
35
+ AsyncDriverAdapterProtocol[AiosqliteConnection, RowT],
36
+ SQLTranslatorMixin,
37
+ TypeCoercionMixin,
38
+ AsyncStorageMixin,
39
+ AsyncPipelinedExecutionMixin,
40
+ ToSchemaMixin,
29
41
  ):
30
- """SQLite Async Driver Adapter."""
42
+ """Aiosqlite SQLite Driver Adapter. Modern protocol implementation."""
31
43
 
32
- connection: "AiosqliteConnection"
33
- dialect: str = "sqlite"
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__(self, connection: "AiosqliteConnection") -> None:
36
- self.connection = connection
37
-
38
- @staticmethod
39
- async def _cursor(connection: "AiosqliteConnection", *args: Any, **kwargs: Any) -> "aiosqlite.Cursor":
40
- return await connection.cursor(*args, **kwargs)
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 _with_cursor(self, connection: "AiosqliteConnection") -> "AsyncGenerator[aiosqlite.Cursor, None]":
44
- cursor = await self._cursor(connection)
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 _process_sql_params(
51
- self,
52
- sql: str,
53
- parameters: "Optional[StatementParameterType]" = None,
54
- *filters: "StatementFilter",
55
- **kwargs: Any,
56
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
57
- """Process SQL and parameters for aiosqlite using SQLStatement.
58
-
59
- aiosqlite supports both named (:name) and positional (?) parameters.
60
- This method processes the SQL with dialect-aware parsing and handles
61
- parameters appropriately for aiosqlite.
62
-
63
- Args:
64
- sql: SQL statement.
65
- parameters: Query parameters. Can be data or a StatementFilter.
66
- *filters: Statement filters to apply.
67
- **kwargs: Additional keyword arguments.
68
-
69
- Returns:
70
- Tuple of processed SQL and parameters.
71
- """
72
- passed_parameters: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
73
- combined_filters_list: list[StatementFilter] = list(filters)
74
-
75
- if parameters is not None:
76
- if isinstance(parameters, StatementFilter):
77
- combined_filters_list.insert(0, parameters)
78
- # _actual_data_params remains None
79
- else:
80
- # If parameters is not a StatementFilter, it's actual data parameters.
81
- passed_parameters = parameters
82
-
83
- statement = SQLStatement(sql, passed_parameters, kwargs=kwargs, dialect=self.dialect)
84
-
85
- for filter_obj in combined_filters_list:
86
- statement = statement.apply_filter(filter_obj)
87
-
88
- processed_sql, processed_params, parsed_expr = statement.process()
89
- if processed_params is None:
90
- return processed_sql, None
91
-
92
- if is_dict(processed_params):
93
- # For dict parameters, we need to use ordered ? placeholders
94
- # but only if we have a parsed expression to work with
95
- if parsed_expr:
96
- # Collect named parameters in the order they appear in the SQL
97
- named_params = []
98
- for node in parsed_expr.find_all(exp.Parameter, exp.Placeholder):
99
- if isinstance(node, exp.Parameter) and node.name and node.name in processed_params:
100
- named_params.append(node.name)
101
- elif (
102
- isinstance(node, exp.Placeholder)
103
- and isinstance(node.this, str)
104
- and node.this in processed_params
105
- ):
106
- named_params.append(node.this)
107
-
108
- if named_params:
109
- # Transform SQL to use ? placeholders
110
- def _convert_to_qmark(node: exp.Expression) -> exp.Expression:
111
- if (isinstance(node, exp.Parameter) and node.name and node.name in processed_params) or (
112
- isinstance(node, exp.Placeholder)
113
- and isinstance(node.this, str)
114
- and node.this in processed_params
115
- ):
116
- return exp.Placeholder() # ? placeholder
117
- return node
118
-
119
- return parsed_expr.transform(_convert_to_qmark, copy=True).sql(dialect=self.dialect), tuple(
120
- processed_params[name] for name in named_params
121
- )
122
- return processed_sql, processed_params
123
- if isinstance(processed_params, (list, tuple)):
124
- return processed_sql, tuple(processed_params)
125
- return processed_sql, (processed_params,)
126
-
127
- # --- Public API Methods --- #
128
- @overload
129
- async def select(
130
- self,
131
- sql: str,
132
- parameters: "Optional[StatementParameterType]" = None,
133
- *filters: "StatementFilter",
134
- connection: "Optional[AiosqliteConnection]" = None,
135
- schema_type: None = None,
136
- **kwargs: Any,
137
- ) -> "Sequence[dict[str, Any]]": ...
138
- @overload
139
- async def select(
140
- self,
141
- sql: str,
142
- parameters: "Optional[StatementParameterType]" = None,
143
- *filters: "StatementFilter",
144
- connection: "Optional[AiosqliteConnection]" = None,
145
- schema_type: "type[ModelDTOT]",
146
- **kwargs: Any,
147
- ) -> "Sequence[ModelDTOT]": ...
148
- async def select(
149
- self,
150
- sql: str,
151
- parameters: "Optional[StatementParameterType]" = None,
152
- *filters: "StatementFilter",
153
- connection: "Optional[AiosqliteConnection]" = None,
154
- schema_type: "Optional[type[ModelDTOT]]" = None,
155
- **kwargs: Any,
156
- ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
157
- """Fetch data from the database.
158
-
159
- Returns:
160
- List of row data as either model instances or dictionaries.
161
- """
162
- connection = self._connection(connection)
163
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
164
-
165
- async with self._with_cursor(connection) as cursor:
166
- await cursor.execute(sql, parameters or ())
167
- results = await cursor.fetchall()
168
- if not results:
169
- return []
170
- column_names = [column[0] for column in cursor.description]
171
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
172
-
173
- @overload
174
- async def select_one(
175
- self,
176
- sql: str,
177
- parameters: "Optional[StatementParameterType]" = None,
178
- *filters: "StatementFilter",
179
- connection: "Optional[AiosqliteConnection]" = None,
180
- schema_type: None = None,
181
- **kwargs: Any,
182
- ) -> "dict[str, Any]": ...
183
- @overload
184
- async def select_one(
185
- self,
186
- sql: str,
187
- parameters: "Optional[StatementParameterType]" = None,
188
- *filters: "StatementFilter",
189
- connection: "Optional[AiosqliteConnection]" = None,
190
- schema_type: "type[ModelDTOT]",
191
- **kwargs: Any,
192
- ) -> "ModelDTOT": ...
193
- async def select_one(
194
- self,
195
- sql: str,
196
- parameters: "Optional[StatementParameterType]" = None,
197
- *filters: "StatementFilter",
198
- connection: "Optional[AiosqliteConnection]" = None,
199
- schema_type: "Optional[type[ModelDTOT]]" = None,
200
- **kwargs: Any,
201
- ) -> "Union[dict[str, Any], ModelDTOT]":
202
- """Fetch one row from the database.
203
-
204
- Returns:
205
- The first row of the query results.
206
- """
207
- connection = self._connection(connection)
208
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
209
-
210
- async with self._with_cursor(connection) as cursor:
211
- await cursor.execute(sql, parameters or ())
212
- result = await cursor.fetchone()
213
- result = self.check_not_found(result)
214
-
215
- # Get column names
216
- column_names = [column[0] for column in cursor.description]
217
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
218
-
219
- @overload
220
- async def select_one_or_none(
221
- self,
222
- sql: str,
223
- parameters: "Optional[StatementParameterType]" = None,
224
- *filters: "StatementFilter",
225
- connection: "Optional[AiosqliteConnection]" = None,
226
- schema_type: None = None,
227
- **kwargs: Any,
228
- ) -> "Optional[dict[str, Any]]": ...
229
- @overload
230
- async def select_one_or_none(
231
- self,
232
- sql: str,
233
- parameters: "Optional[StatementParameterType]" = None,
234
- *filters: "StatementFilter",
235
- connection: "Optional[AiosqliteConnection]" = None,
236
- schema_type: "type[ModelDTOT]",
237
- **kwargs: Any,
238
- ) -> "Optional[ModelDTOT]": ...
239
- async def select_one_or_none(
240
- self,
241
- sql: str,
242
- parameters: "Optional[StatementParameterType]" = None,
243
- *filters: "StatementFilter",
244
- connection: "Optional[AiosqliteConnection]" = None,
245
- schema_type: "Optional[type[ModelDTOT]]" = None,
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