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.

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 +741 -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.0.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.0.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.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,426 +1,263 @@
1
- import logging
1
+ import contextlib
2
+ import csv
2
3
  import sqlite3
4
+ from collections.abc import Iterator
3
5
  from contextlib import contextmanager
4
- from sqlite3 import Cursor
5
- from typing import TYPE_CHECKING, Any, Optional, Union, overload
6
-
7
- from sqlspec.base import SyncDriverAdapterProtocol
8
- from sqlspec.filters import StatementFilter
9
- from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
10
- from sqlspec.statement import SQLStatement
11
- from sqlspec.typing import is_dict
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
8
+
9
+ from typing_extensions import TypeAlias
10
+
11
+ from sqlspec.driver import SyncDriverAdapterProtocol
12
+ from sqlspec.driver.mixins import (
13
+ SQLTranslatorMixin,
14
+ SyncPipelinedExecutionMixin,
15
+ SyncStorageMixin,
16
+ ToSchemaMixin,
17
+ TypeCoercionMixin,
18
+ )
19
+ from sqlspec.statement.parameters import ParameterStyle
20
+ from sqlspec.statement.result import DMLResultDict, ScriptResultDict, SelectResultDict, SQLResult
21
+ from sqlspec.statement.sql import SQL, SQLConfig
22
+ from sqlspec.typing import DictRow, ModelDTOT, RowT, is_dict_with_field
23
+ from sqlspec.utils.logging import get_logger
24
+ from sqlspec.utils.serializers import to_json
12
25
 
13
26
  if TYPE_CHECKING:
14
- from collections.abc import Generator, Mapping, Sequence
15
-
16
- from sqlspec.typing import ModelDTOT, StatementParameterType, T
27
+ from sqlglot.dialects.dialect import DialectType
17
28
 
18
29
  __all__ = ("SqliteConnection", "SqliteDriver")
19
30
 
20
- logger = logging.getLogger("sqlspec")
31
+ logger = get_logger("adapters.sqlite")
21
32
 
22
- SqliteConnection = sqlite3.Connection
33
+ SqliteConnection: TypeAlias = sqlite3.Connection
23
34
 
24
35
 
25
36
  class SqliteDriver(
26
- SQLTranslatorMixin["SqliteConnection"],
27
- SyncDriverAdapterProtocol["SqliteConnection"],
28
- ResultConverter,
37
+ SyncDriverAdapterProtocol[SqliteConnection, RowT],
38
+ SQLTranslatorMixin,
39
+ TypeCoercionMixin,
40
+ SyncStorageMixin,
41
+ SyncPipelinedExecutionMixin,
42
+ ToSchemaMixin,
29
43
  ):
30
- """SQLite Sync Driver Adapter."""
44
+ """SQLite Sync Driver Adapter with Arrow/Parquet export support.
31
45
 
32
- connection: "SqliteConnection"
33
- dialect: str = "sqlite"
46
+ Refactored to align with the new enhanced driver architecture and
47
+ instrumentation standards following the psycopg pattern.
48
+ """
34
49
 
35
- def __init__(self, connection: "SqliteConnection") -> None:
36
- self.connection = connection
50
+ __slots__ = ()
37
51
 
38
- @staticmethod
39
- def _cursor(connection: "SqliteConnection", *args: Any, **kwargs: Any) -> Cursor:
40
- return connection.cursor(*args, **kwargs) # type: ignore[no-any-return]
52
+ dialect: "DialectType" = "sqlite"
53
+ supported_parameter_styles: "tuple[ParameterStyle, ...]" = (ParameterStyle.QMARK, ParameterStyle.NAMED_COLON)
54
+ default_parameter_style: ParameterStyle = ParameterStyle.QMARK
55
+
56
+ def __init__(
57
+ self,
58
+ connection: "SqliteConnection",
59
+ config: "Optional[SQLConfig]" = None,
60
+ default_row_type: "type[DictRow]" = dict[str, Any],
61
+ ) -> None:
62
+ super().__init__(connection=connection, config=config, default_row_type=default_row_type)
63
+
64
+ # SQLite-specific type coercion overrides
65
+ def _coerce_boolean(self, value: Any) -> Any:
66
+ """SQLite stores booleans as integers (0/1)."""
67
+ if isinstance(value, bool):
68
+ return 1 if value else 0
69
+ return value
70
+
71
+ def _coerce_decimal(self, value: Any) -> Any:
72
+ """SQLite stores decimals as strings to preserve precision."""
73
+ if isinstance(value, str):
74
+ return value # Already a string
75
+ from decimal import Decimal
76
+
77
+ if isinstance(value, Decimal):
78
+ return str(value)
79
+ return value
80
+
81
+ def _coerce_json(self, value: Any) -> Any:
82
+ """SQLite stores JSON as strings (requires JSON1 extension)."""
83
+ if isinstance(value, (dict, list)):
84
+ return to_json(value)
85
+ return value
86
+
87
+ def _coerce_array(self, value: Any) -> Any:
88
+ """SQLite doesn't have native arrays - store as JSON strings."""
89
+ if isinstance(value, (list, tuple)):
90
+ return to_json(list(value))
91
+ return value
41
92
 
93
+ @staticmethod
42
94
  @contextmanager
43
- def _with_cursor(self, connection: "SqliteConnection") -> "Generator[Cursor, None, None]":
44
- cursor = self._cursor(connection)
95
+ def _get_cursor(connection: SqliteConnection) -> Iterator[sqlite3.Cursor]:
96
+ cursor = connection.cursor()
45
97
  try:
46
98
  yield cursor
47
99
  finally:
48
- cursor.close()
49
-
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 SQLite using SQLStatement.
58
-
59
- SQLite supports both named (:name) and positional (?) parameters.
60
- This method processes the SQL with dialect-aware parsing and handles
61
- parameters appropriately for SQLite.
62
-
63
- Args:
64
- sql: The SQL to process.
65
- parameters: The parameters to process.
66
- *filters: Statement filters to apply.
67
- **kwargs: Additional keyword arguments.
68
-
69
- Returns:
70
- A tuple of (processed SQL, processed parameters).
71
- """
72
- # Create a SQLStatement with SQLite dialect
73
- data_params_for_statement: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
74
- combined_filters_list: list[StatementFilter] = list(filters)
75
-
76
- if parameters is not None:
77
- if isinstance(parameters, StatementFilter):
78
- combined_filters_list.insert(0, parameters)
100
+ with contextlib.suppress(Exception):
101
+ cursor.close()
102
+
103
+ def _execute_statement(
104
+ self, statement: SQL, connection: Optional[SqliteConnection] = None, **kwargs: Any
105
+ ) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
106
+ if statement.is_script:
107
+ sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
108
+ return self._execute_script(sql, connection=connection, **kwargs)
109
+
110
+ # Determine if we need to convert parameter style
111
+ detected_styles = {p.style for p in statement.parameter_info}
112
+ target_style = self.default_parameter_style
113
+
114
+ # Check if any detected style is not supported
115
+ unsupported_styles = detected_styles - set(self.supported_parameter_styles)
116
+ if unsupported_styles:
117
+ # Convert to default style if we have unsupported styles
118
+ target_style = self.default_parameter_style
119
+ elif len(detected_styles) > 1:
120
+ # Mixed styles detected - use default style for consistency
121
+ target_style = self.default_parameter_style
122
+ elif detected_styles:
123
+ # Single style detected - use it if supported
124
+ single_style = next(iter(detected_styles))
125
+ if single_style in self.supported_parameter_styles:
126
+ target_style = single_style
79
127
  else:
80
- data_params_for_statement = parameters
81
- if data_params_for_statement is not None and not isinstance(data_params_for_statement, (list, tuple, dict)):
82
- data_params_for_statement = (data_params_for_statement,)
83
- statement = SQLStatement(sql, data_params_for_statement, 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, _ = statement.process()
89
-
90
- if processed_params is None:
91
- return processed_sql, None
92
-
93
- if is_dict(processed_params):
94
- return processed_sql, processed_params
95
-
96
- if isinstance(processed_params, (list, tuple)):
97
- return processed_sql, tuple(processed_params)
98
-
99
- return processed_sql, (processed_params,)
100
-
101
- # --- Public API Methods --- #
102
- @overload
103
- def select(
104
- self,
105
- sql: str,
106
- parameters: "Optional[StatementParameterType]" = None,
107
- *filters: "StatementFilter",
108
- connection: "Optional[SqliteConnection]" = None,
109
- schema_type: None = None,
110
- **kwargs: Any,
111
- ) -> "Sequence[dict[str, Any]]": ...
112
- @overload
113
- def select(
114
- self,
115
- sql: str,
116
- parameters: "Optional[StatementParameterType]" = None,
117
- *filters: "StatementFilter",
118
- connection: "Optional[SqliteConnection]" = None,
119
- schema_type: "type[ModelDTOT]",
120
- **kwargs: Any,
121
- ) -> "Sequence[ModelDTOT]": ...
122
- def select(
123
- self,
124
- sql: str,
125
- parameters: "Optional[StatementParameterType]" = None,
126
- *filters: "StatementFilter",
127
- connection: "Optional[SqliteConnection]" = None,
128
- schema_type: "Optional[type[ModelDTOT]]" = None,
129
- **kwargs: Any,
130
- ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
131
- """Fetch data from the database.
132
-
133
- Returns:
134
- List of row data as either model instances or dictionaries.
135
- """
136
- connection = self._connection(connection)
137
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
138
-
139
- with self._with_cursor(connection) as cursor:
140
- cursor.execute(sql, parameters or [])
141
- results = cursor.fetchall()
142
- if not results:
143
- return []
144
-
145
- # Get column names
146
- column_names = [column[0] for column in cursor.description]
147
-
148
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
149
-
150
- @overload
151
- def select_one(
152
- self,
153
- sql: str,
154
- parameters: "Optional[StatementParameterType]" = None,
155
- *filters: "StatementFilter",
156
- connection: "Optional[SqliteConnection]" = None,
157
- schema_type: None = None,
158
- **kwargs: Any,
159
- ) -> "dict[str, Any]": ...
160
- @overload
161
- def select_one(
162
- self,
163
- sql: str,
164
- parameters: "Optional[StatementParameterType]" = None,
165
- *filters: "StatementFilter",
166
- connection: "Optional[SqliteConnection]" = None,
167
- schema_type: "type[ModelDTOT]",
168
- **kwargs: Any,
169
- ) -> "ModelDTOT": ...
170
- def select_one(
171
- self,
172
- sql: str,
173
- parameters: "Optional[StatementParameterType]" = None,
174
- *filters: "StatementFilter",
175
- connection: "Optional[SqliteConnection]" = None,
176
- schema_type: "Optional[type[ModelDTOT]]" = None,
177
- **kwargs: Any,
178
- ) -> "Union[dict[str, Any], ModelDTOT]":
179
- """Fetch one row from the database.
180
-
181
- Returns:
182
- The first row of the query results.
183
- """
184
- connection = self._connection(connection)
185
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
186
-
187
- # Execute the query
188
- cursor = connection.cursor()
189
- cursor.execute(sql, parameters or [])
190
- result = cursor.fetchone()
191
- result = self.check_not_found(result)
192
- column_names = [column[0] for column in cursor.description]
193
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
194
-
195
- @overload
196
- def select_one_or_none(
197
- self,
198
- sql: str,
199
- parameters: "Optional[StatementParameterType]" = None,
200
- *filters: "StatementFilter",
201
- connection: "Optional[SqliteConnection]" = None,
202
- schema_type: None = None,
203
- **kwargs: Any,
204
- ) -> "Optional[dict[str, Any]]": ...
205
- @overload
206
- def select_one_or_none(
207
- self,
208
- sql: str,
209
- parameters: "Optional[StatementParameterType]" = None,
210
- *filters: "StatementFilter",
211
- connection: "Optional[SqliteConnection]" = None,
212
- schema_type: "type[ModelDTOT]",
213
- **kwargs: Any,
214
- ) -> "Optional[ModelDTOT]": ...
215
- def select_one_or_none(
216
- self,
217
- sql: str,
218
- parameters: "Optional[StatementParameterType]" = None,
219
- *filters: "StatementFilter",
220
- connection: "Optional[SqliteConnection]" = None,
221
- schema_type: "Optional[type[ModelDTOT]]" = None,
222
- **kwargs: Any,
223
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
224
- """Fetch one row from the database.
225
-
226
- Returns:
227
- The first row of the query results, or None if no results.
228
- """
229
- connection = self._connection(connection)
230
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
231
-
232
- with self._with_cursor(connection) as cursor:
233
- cursor.execute(sql, parameters or [])
234
- result = cursor.fetchone()
235
- if result is None:
236
- return None
237
-
238
- column_names = [column[0] for column in cursor.description]
239
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
240
-
241
- @overload
242
- def select_value(
243
- self,
244
- sql: str,
245
- parameters: "Optional[StatementParameterType]" = None,
246
- *filters: "StatementFilter",
247
- connection: "Optional[SqliteConnection]" = None,
248
- schema_type: None = None,
249
- **kwargs: Any,
250
- ) -> "Any": ...
251
- @overload
252
- def select_value(
253
- self,
254
- sql: str,
255
- parameters: "Optional[StatementParameterType]" = None,
256
- *filters: "StatementFilter",
257
- connection: "Optional[SqliteConnection]" = None,
258
- schema_type: "type[T]",
259
- **kwargs: Any,
260
- ) -> "T": ...
261
- def select_value(
262
- self,
263
- sql: str,
264
- parameters: "Optional[StatementParameterType]" = None,
265
- *filters: "StatementFilter",
266
- connection: "Optional[SqliteConnection]" = None,
267
- schema_type: "Optional[type[T]]" = None,
268
- **kwargs: Any,
269
- ) -> "Union[T, Any]":
270
- """Fetch a single value from the database.
271
-
272
- Returns:
273
- The first value from the first row of results.
274
- """
275
- connection = self._connection(connection)
276
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
277
-
278
- with self._with_cursor(connection) as cursor:
279
- cursor.execute(sql, parameters or [])
280
- result = cursor.fetchone()
281
- result = self.check_not_found(result)
282
- result_value = result[0]
283
- if schema_type is None:
284
- return result_value
285
- return schema_type(result_value) # type: ignore[call-arg]
286
-
287
- @overload
288
- def select_value_or_none(
289
- self,
290
- sql: str,
291
- parameters: "Optional[StatementParameterType]" = None,
292
- *filters: "StatementFilter",
293
- connection: "Optional[SqliteConnection]" = None,
294
- schema_type: None = None,
295
- **kwargs: Any,
296
- ) -> "Optional[Any]": ...
297
- @overload
298
- def select_value_or_none(
299
- self,
300
- sql: str,
301
- parameters: "Optional[StatementParameterType]" = None,
302
- *filters: "StatementFilter",
303
- connection: "Optional[SqliteConnection]" = None,
304
- schema_type: "type[T]",
305
- **kwargs: Any,
306
- ) -> "Optional[T]": ...
307
- def select_value_or_none(
308
- self,
309
- sql: str,
310
- parameters: "Optional[StatementParameterType]" = None,
311
- *filters: "StatementFilter",
312
- connection: "Optional[SqliteConnection]" = None,
313
- schema_type: "Optional[type[T]]" = None,
314
- **kwargs: Any,
315
- ) -> "Optional[Union[T, Any]]":
316
- """Fetch a single value from the database.
317
-
318
- Returns:
319
- The first value from the first row of results, or None if no results.
320
- """
321
- connection = self._connection(connection)
322
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
323
-
324
- with self._with_cursor(connection) as cursor:
325
- cursor.execute(sql, parameters or [])
326
- result = cursor.fetchone()
327
- if result is None:
328
- return None
329
- result_value = result[0]
330
- if schema_type is None:
331
- return result_value
332
- return schema_type(result_value) # type: ignore[call-arg]
333
-
334
- def insert_update_delete(
335
- self,
336
- sql: str,
337
- parameters: "Optional[StatementParameterType]" = None,
338
- *filters: "StatementFilter",
339
- connection: "Optional[SqliteConnection]" = None,
340
- **kwargs: Any,
341
- ) -> int:
342
- """Insert, update, or delete data from the database.
343
-
344
- Returns:
345
- Row count affected by the operation.
346
- """
347
- connection = self._connection(connection)
348
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
349
-
350
- with self._with_cursor(connection) as cursor:
351
- cursor.execute(sql, parameters or [])
352
- return cursor.rowcount
353
-
354
- @overload
355
- def insert_update_delete_returning(
356
- self,
357
- sql: str,
358
- parameters: "Optional[StatementParameterType]" = None,
359
- *filters: "StatementFilter",
360
- connection: "Optional[SqliteConnection]" = None,
361
- schema_type: None = None,
362
- **kwargs: Any,
363
- ) -> "dict[str, Any]": ...
364
- @overload
365
- def insert_update_delete_returning(
366
- self,
367
- sql: str,
368
- parameters: "Optional[StatementParameterType]" = None,
369
- *filters: "StatementFilter",
370
- connection: "Optional[SqliteConnection]" = None,
371
- schema_type: "type[ModelDTOT]",
372
- **kwargs: Any,
373
- ) -> "ModelDTOT": ...
374
- def insert_update_delete_returning(
375
- self,
376
- sql: str,
377
- parameters: "Optional[StatementParameterType]" = None,
378
- *filters: "StatementFilter",
379
- connection: "Optional[SqliteConnection]" = None,
380
- schema_type: "Optional[type[ModelDTOT]]" = None,
381
- **kwargs: Any,
382
- ) -> "Union[dict[str, Any], ModelDTOT]":
383
- """Insert, update, or delete data from the database and return result.
384
-
385
- Returns:
386
- The first row of results.
387
- """
388
- connection = self._connection(connection)
389
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
390
-
391
- with self._with_cursor(connection) as cursor:
392
- cursor.execute(sql, parameters or [])
393
- result = cursor.fetchone()
394
- result = self.check_not_found(result)
395
- column_names = [column[0] for column in cursor.description]
396
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
397
-
398
- def execute_script(
399
- self,
400
- sql: str,
401
- parameters: "Optional[StatementParameterType]" = None,
402
- connection: "Optional[SqliteConnection]" = None,
403
- **kwargs: Any,
404
- ) -> str:
405
- """Execute a script.
406
-
407
- Returns:
408
- Status message for the operation.
409
- """
410
- connection = self._connection(connection)
411
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
412
-
413
- with self._with_cursor(connection) as cursor:
414
- cursor.executescript(sql)
415
- return "DONE"
416
-
417
- def _connection(self, connection: "Optional[SqliteConnection]" = None) -> "SqliteConnection":
418
- """Get the connection to use for the operation.
419
-
420
- Args:
421
- connection: Optional connection to use.
422
-
423
- Returns:
424
- The connection to use.
425
- """
426
- return connection or self.connection
128
+ target_style = self.default_parameter_style
129
+
130
+ if statement.is_many:
131
+ sql, params = statement.compile(placeholder_style=target_style)
132
+ return self._execute_many(sql, params, connection=connection, **kwargs)
133
+
134
+ sql, params = statement.compile(placeholder_style=target_style)
135
+
136
+ # Process parameters through type coercion
137
+ params = self._process_parameters(params)
138
+
139
+ # SQLite expects tuples for positional parameters
140
+ if isinstance(params, list):
141
+ params = tuple(params)
142
+
143
+ return self._execute(sql, params, statement, connection=connection, **kwargs)
144
+
145
+ def _execute(
146
+ self, sql: str, parameters: Any, statement: SQL, connection: Optional[SqliteConnection] = None, **kwargs: Any
147
+ ) -> Union[SelectResultDict, DMLResultDict]:
148
+ """Execute a single statement with parameters."""
149
+ conn = self._connection(connection)
150
+ with self._get_cursor(conn) as cursor:
151
+ # SQLite expects tuple or dict parameters
152
+ if parameters is not None and not isinstance(parameters, (tuple, list, dict)):
153
+ # Convert scalar to tuple
154
+ parameters = (parameters,)
155
+ cursor.execute(sql, parameters or ())
156
+ if self.returns_rows(statement.expression):
157
+ fetched_data: list[sqlite3.Row] = cursor.fetchall()
158
+ return {
159
+ "data": fetched_data,
160
+ "column_names": [col[0] for col in cursor.description or []],
161
+ "rows_affected": len(fetched_data),
162
+ }
163
+ return {"rows_affected": cursor.rowcount, "status_message": "OK"}
164
+
165
+ def _execute_many(
166
+ self, sql: str, param_list: Any, connection: Optional[SqliteConnection] = None, **kwargs: Any
167
+ ) -> DMLResultDict:
168
+ """Execute a statement many times with a list of parameter tuples."""
169
+ conn = self._connection(connection)
170
+ if param_list:
171
+ param_list = self._process_parameters(param_list)
172
+
173
+ # Convert parameter list to proper format for executemany
174
+ formatted_params: list[tuple[Any, ...]] = []
175
+ if param_list and isinstance(param_list, list):
176
+ for param_set in cast("list[Union[list, tuple]]", param_list):
177
+ if isinstance(param_set, (list, tuple)):
178
+ formatted_params.append(tuple(param_set))
179
+ elif param_set is None:
180
+ formatted_params.append(())
181
+ else:
182
+ formatted_params.append((param_set,))
183
+
184
+ with self._get_cursor(conn) as cursor:
185
+ cursor.executemany(sql, formatted_params)
186
+ return {"rows_affected": cursor.rowcount, "status_message": "OK"}
187
+
188
+ def _execute_script(
189
+ self, script: str, connection: Optional[SqliteConnection] = None, **kwargs: Any
190
+ ) -> ScriptResultDict:
191
+ """Execute a script on the SQLite connection."""
192
+ conn = self._connection(connection)
193
+ with self._get_cursor(conn) as cursor:
194
+ cursor.executescript(script)
195
+ # executescript doesn't auto-commit in some cases
196
+ conn.commit()
197
+ result: ScriptResultDict = {"statements_executed": -1, "status_message": "SCRIPT EXECUTED"}
198
+ return result
199
+
200
+ def _bulk_load_file(self, file_path: Path, table_name: str, format: str, mode: str, **options: Any) -> int:
201
+ """Database-specific bulk load implementation."""
202
+ if format != "csv":
203
+ msg = f"SQLite driver only supports CSV for bulk loading, not {format}."
204
+ raise NotImplementedError(msg)
205
+
206
+ conn = self._connection(None)
207
+ with self._get_cursor(conn) as cursor:
208
+ if mode == "replace":
209
+ cursor.execute(f"DELETE FROM {table_name}")
210
+
211
+ with Path(file_path).open(encoding="utf-8") as f:
212
+ reader = csv.reader(f, **options)
213
+ header = next(reader) # Skip header
214
+ placeholders = ", ".join("?" for _ in header)
215
+ sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
216
+
217
+ # executemany is efficient for bulk inserts
218
+ data_iter = list(reader) # Read all data into memory
219
+ cursor.executemany(sql, data_iter)
220
+ return cursor.rowcount
221
+
222
+ def _wrap_select_result(
223
+ self, statement: SQL, result: SelectResultDict, schema_type: Optional[type[ModelDTOT]] = None, **kwargs: Any
224
+ ) -> Union[SQLResult[ModelDTOT], SQLResult[RowT]]:
225
+ rows_as_dicts = [dict(row) for row in result["data"]]
226
+ if schema_type:
227
+ return SQLResult[ModelDTOT](
228
+ statement=statement,
229
+ data=list(self.to_schema(data=rows_as_dicts, schema_type=schema_type)),
230
+ column_names=result["column_names"],
231
+ rows_affected=result["rows_affected"],
232
+ operation_type="SELECT",
233
+ )
234
+
235
+ return SQLResult[RowT](
236
+ statement=statement,
237
+ data=rows_as_dicts,
238
+ column_names=result["column_names"],
239
+ rows_affected=result["rows_affected"],
240
+ operation_type="SELECT",
241
+ )
242
+
243
+ def _wrap_execute_result(
244
+ self, statement: SQL, result: Union[DMLResultDict, ScriptResultDict], **kwargs: Any
245
+ ) -> SQLResult[RowT]:
246
+ if is_dict_with_field(result, "statements_executed"):
247
+ return SQLResult[RowT](
248
+ statement=statement,
249
+ data=[],
250
+ rows_affected=0,
251
+ operation_type="SCRIPT",
252
+ metadata={
253
+ "status_message": result.get("status_message", ""),
254
+ "statements_executed": result.get("statements_executed", -1),
255
+ },
256
+ )
257
+ return SQLResult[RowT](
258
+ statement=statement,
259
+ data=[],
260
+ rows_affected=cast("int", result.get("rows_affected", -1)),
261
+ operation_type=statement.expression.key.upper() if statement.expression else "UNKNOWN",
262
+ metadata={"status_message": result.get("status_message", "")},
263
+ )