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,462 +1,244 @@
1
- # type: ignore
2
1
  import logging
3
- import re
4
- from collections.abc import AsyncGenerator
2
+ from collections.abc import AsyncGenerator, Sequence
5
3
  from contextlib import asynccontextmanager
6
- from typing import TYPE_CHECKING, Any, Optional, Union, overload
4
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
7
5
 
8
6
  from asyncmy import Connection
9
-
10
- from sqlspec.base import AsyncDriverAdapterProtocol
11
- from sqlspec.exceptions import ParameterStyleMismatchError
12
- from sqlspec.filters import StatementFilter
13
- from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
14
- from sqlspec.statement import SQLStatement
15
- from sqlspec.typing import is_dict
7
+ from typing_extensions import TypeAlias
8
+
9
+ from sqlspec.driver import AsyncDriverAdapterProtocol
10
+ from sqlspec.driver.mixins import (
11
+ AsyncPipelinedExecutionMixin,
12
+ AsyncStorageMixin,
13
+ SQLTranslatorMixin,
14
+ ToSchemaMixin,
15
+ TypeCoercionMixin,
16
+ )
17
+ from sqlspec.statement.parameters import ParameterStyle
18
+ from sqlspec.statement.result import DMLResultDict, ScriptResultDict, SelectResultDict, SQLResult
19
+ from sqlspec.statement.sql import SQL, SQLConfig
20
+ from sqlspec.typing import DictRow, ModelDTOT, RowT
16
21
 
17
22
  if TYPE_CHECKING:
18
- from collections.abc import Mapping, Sequence
19
-
20
- from asyncmy.cursors import Cursor
21
-
22
- from sqlspec.typing import ModelDTOT, StatementParameterType, T
23
+ from asyncmy.cursors import Cursor, DictCursor
24
+ from sqlglot.dialects.dialect import DialectType
23
25
 
24
- __all__ = ("AsyncmyDriver",)
25
-
26
- AsyncmyConnection = Connection
26
+ __all__ = ("AsyncmyConnection", "AsyncmyDriver")
27
27
 
28
28
  logger = logging.getLogger("sqlspec")
29
29
 
30
- # Pattern to identify MySQL-style placeholders (%s) for proper conversion
31
- MYSQL_PLACEHOLDER_PATTERN = re.compile(r"(?<!%)%s")
30
+ AsyncmyConnection: TypeAlias = Connection
32
31
 
33
32
 
34
33
  class AsyncmyDriver(
35
- SQLTranslatorMixin["AsyncmyConnection"],
36
- AsyncDriverAdapterProtocol["AsyncmyConnection"],
37
- ResultConverter,
34
+ AsyncDriverAdapterProtocol[AsyncmyConnection, RowT],
35
+ SQLTranslatorMixin,
36
+ TypeCoercionMixin,
37
+ AsyncStorageMixin,
38
+ AsyncPipelinedExecutionMixin,
39
+ ToSchemaMixin,
38
40
  ):
39
- """Asyncmy MySQL/MariaDB Driver Adapter."""
41
+ """Asyncmy MySQL/MariaDB Driver Adapter. Modern protocol implementation."""
40
42
 
41
- connection: "AsyncmyConnection"
42
- dialect: str = "mysql"
43
+ dialect: "DialectType" = "mysql"
44
+ supported_parameter_styles: "tuple[ParameterStyle, ...]" = (ParameterStyle.POSITIONAL_PYFORMAT,)
45
+ default_parameter_style: ParameterStyle = ParameterStyle.POSITIONAL_PYFORMAT
46
+ __supports_arrow__: ClassVar[bool] = True
47
+ __supports_parquet__: ClassVar[bool] = False
48
+ __slots__ = ()
43
49
 
44
- def __init__(self, connection: "AsyncmyConnection") -> None:
45
- self.connection = connection
50
+ def __init__(
51
+ self,
52
+ connection: AsyncmyConnection,
53
+ config: Optional[SQLConfig] = None,
54
+ default_row_type: type[DictRow] = DictRow,
55
+ ) -> None:
56
+ super().__init__(connection=connection, config=config, default_row_type=default_row_type)
46
57
 
47
- @staticmethod
48
58
  @asynccontextmanager
49
- async def _with_cursor(connection: "AsyncmyConnection") -> AsyncGenerator["Cursor", None]:
50
- cursor = connection.cursor()
59
+ async def _get_cursor(
60
+ self, connection: "Optional[AsyncmyConnection]" = None
61
+ ) -> "AsyncGenerator[Union[Cursor, DictCursor], None]":
62
+ conn = self._connection(connection)
63
+ cursor = await conn.cursor()
51
64
  try:
52
65
  yield cursor
53
66
  finally:
54
67
  await cursor.close()
55
68
 
56
- def _process_sql_params(
57
- self,
58
- sql: str,
59
- parameters: "Optional[StatementParameterType]" = None,
60
- *filters: "StatementFilter",
61
- **kwargs: Any,
62
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
63
- """Process SQL and parameters using SQLStatement with dialect support.
64
-
65
- Args:
66
- sql: The SQL statement to process.
67
- parameters: The parameters to bind to the statement. Can be data or a StatementFilter.
68
- *filters: Statement filters to apply.
69
- **kwargs: Additional keyword arguments.
70
-
71
- Raises:
72
- ParameterStyleMismatchError: If the parameter style is not supported.
73
-
74
- Returns:
75
- A tuple of (sql, parameters) ready for execution.
76
- """
77
- # Convert filters tuple to a list to allow modification
78
- current_filters: list[StatementFilter] = list(filters)
79
- actual_parameters: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
80
-
81
- if parameters is not None:
82
- if isinstance(parameters, StatementFilter):
83
- current_filters.insert(0, parameters)
84
- # actual_parameters remains None
85
- else:
86
- actual_parameters = parameters # type: ignore[assignment]
87
-
88
- # Handle MySQL-specific placeholders (%s) which SQLGlot doesn't parse well
89
- # If %s placeholders are present, handle them directly
90
- mysql_placeholders_count = len(MYSQL_PLACEHOLDER_PATTERN.findall(sql))
91
-
92
- if mysql_placeholders_count > 0:
93
- # For MySQL format placeholders, minimal processing is needed
94
- if actual_parameters is None:
95
- if mysql_placeholders_count > 0:
96
- msg = f"asyncmy: SQL statement contains {mysql_placeholders_count} format placeholders ('%s'), but no parameters were provided. SQL: {sql}"
97
- raise ParameterStyleMismatchError(msg)
98
- return sql, None
99
-
100
- # Convert dict to tuple if needed
101
- if is_dict(actual_parameters):
102
- # MySQL's %s placeholders require positional params
103
- msg = "asyncmy: Dictionary parameters provided with '%s' placeholders. MySQL format placeholders require tuple/list parameters."
104
- raise ParameterStyleMismatchError(msg)
105
-
106
- # Convert to tuple (handles both scalar and sequence cases)
107
- if not isinstance(actual_parameters, (list, tuple)):
108
- # Scalar parameter case
109
- return sql, (actual_parameters,)
110
-
111
- # Sequence parameter case - ensure appropriate length
112
- if len(actual_parameters) != mysql_placeholders_count: # type: ignore[arg-type]
113
- msg = f"asyncmy: Parameter count mismatch. SQL expects {mysql_placeholders_count} '%s' placeholders, but {len(actual_parameters)} parameters were provided. SQL: {sql}" # type: ignore[arg-type]
114
- raise ParameterStyleMismatchError(msg)
115
-
116
- return sql, tuple(actual_parameters) # type: ignore[arg-type]
117
-
118
- # Create a SQLStatement with MySQL dialect
119
- statement = SQLStatement(sql, actual_parameters, kwargs=kwargs, dialect=self.dialect)
120
-
121
- # Apply any filters
122
- for filter_obj in current_filters: # Use the modified list of filters
123
- statement = statement.apply_filter(filter_obj)
124
-
125
- # Process the statement for execution
126
- processed_sql, processed_params, _ = statement.process()
127
-
128
- # Convert parameters to the format expected by MySQL
129
- if processed_params is None:
130
- return processed_sql, None
131
-
132
- # For MySQL, ensure parameters are in the right format
133
- if is_dict(processed_params):
134
- # Dictionary parameters are not well supported by asyncmy
135
- msg = "asyncmy: Dictionary parameters are not supported for MySQL placeholders. Use sequence parameters."
136
- raise ParameterStyleMismatchError(msg)
137
-
138
- # For sequence parameters, ensure they're a tuple
139
- if isinstance(processed_params, (list, tuple)):
140
- return processed_sql, tuple(processed_params)
141
-
142
- # For scalar parameter, wrap in a tuple
143
- return processed_sql, (processed_params,)
144
-
145
- # --- Public API Methods --- #
146
- @overload
147
- async def select(
148
- self,
149
- sql: str,
150
- parameters: "Optional[StatementParameterType]" = None,
151
- *filters: "StatementFilter",
152
- connection: "Optional[AsyncmyConnection]" = None,
153
- schema_type: None = None,
154
- **kwargs: Any,
155
- ) -> "Sequence[dict[str, Any]]": ...
156
- @overload
157
- async def select(
158
- self,
159
- sql: str,
160
- parameters: "Optional[StatementParameterType]" = None,
161
- *filters: "StatementFilter",
162
- connection: "Optional[AsyncmyConnection]" = None,
163
- schema_type: "type[ModelDTOT]",
164
- **kwargs: Any,
165
- ) -> "Sequence[ModelDTOT]": ...
166
- async def select(
167
- self,
168
- sql: str,
169
- parameters: "Optional[StatementParameterType]" = None,
170
- *filters: "StatementFilter",
171
- connection: "Optional[AsyncmyConnection]" = None,
172
- schema_type: "Optional[type[ModelDTOT]]" = None,
173
- **kwargs: Any,
174
- ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
175
- """Fetch data from the database.
176
-
177
- Returns:
178
- List of row data as either model instances or dictionaries.
179
- """
180
- connection = self._connection(connection)
181
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
182
- async with self._with_cursor(connection) as cursor:
183
- await cursor.execute(sql, parameters)
184
- results = await cursor.fetchall()
185
- if not results:
186
- return []
187
- column_names = [c[0] for c in cursor.description or []]
188
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
189
-
190
- @overload
191
- async def select_one(
192
- self,
193
- sql: str,
194
- parameters: "Optional[StatementParameterType]" = None,
195
- *filters: "StatementFilter",
196
- connection: "Optional[AsyncmyConnection]" = None,
197
- schema_type: None = None,
198
- **kwargs: Any,
199
- ) -> "dict[str, Any]": ...
200
- @overload
201
- async def select_one(
202
- self,
203
- sql: str,
204
- parameters: "Optional[StatementParameterType]" = None,
205
- *filters: "StatementFilter",
206
- connection: "Optional[AsyncmyConnection]" = None,
207
- schema_type: "type[ModelDTOT]",
208
- **kwargs: Any,
209
- ) -> "ModelDTOT": ...
210
- async def select_one(
211
- self,
212
- sql: str,
213
- parameters: "Optional[StatementParameterType]" = None,
214
- *filters: "StatementFilter",
215
- connection: "Optional[AsyncmyConnection]" = None,
216
- schema_type: "Optional[type[ModelDTOT]]" = None,
217
- **kwargs: Any,
218
- ) -> "Union[dict[str, Any], ModelDTOT]":
219
- """Fetch one row from the database.
220
-
221
- Returns:
222
- The first row of the query results.
223
- """
224
- connection = self._connection(connection)
225
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
226
- async with self._with_cursor(connection) as cursor:
227
- await cursor.execute(sql, parameters)
228
- result = await cursor.fetchone()
229
- result = self.check_not_found(result)
230
- column_names = [c[0] for c in cursor.description or []]
231
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
232
-
233
- @overload
234
- async def select_one_or_none(
235
- self,
236
- sql: str,
237
- parameters: "Optional[StatementParameterType]" = None,
238
- *filters: "StatementFilter",
239
- connection: "Optional[AsyncmyConnection]" = None,
240
- schema_type: None = None,
241
- **kwargs: Any,
242
- ) -> "Optional[dict[str, Any]]": ...
243
- @overload
244
- async def select_one_or_none(
245
- self,
246
- sql: str,
247
- parameters: "Optional[StatementParameterType]" = None,
248
- *filters: "StatementFilter",
249
- connection: "Optional[AsyncmyConnection]" = None,
250
- schema_type: "type[ModelDTOT]",
251
- **kwargs: Any,
252
- ) -> "Optional[ModelDTOT]": ...
253
- async def select_one_or_none(
254
- self,
255
- sql: str,
256
- parameters: "Optional[StatementParameterType]" = None,
257
- *filters: "StatementFilter",
258
- connection: "Optional[AsyncmyConnection]" = None,
259
- schema_type: "Optional[type[ModelDTOT]]" = None,
260
- **kwargs: Any,
261
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
262
- """Fetch one row from the database.
263
-
264
- Returns:
265
- The first row of the query results.
266
- """
267
- connection = self._connection(connection)
268
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
269
- async with self._with_cursor(connection) as cursor:
69
+ async def _execute_statement(
70
+ self, statement: SQL, connection: "Optional[AsyncmyConnection]" = None, **kwargs: Any
71
+ ) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
72
+ if statement.is_script:
73
+ sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
74
+ return await self._execute_script(sql, connection=connection, **kwargs)
75
+
76
+ # Let the SQL object handle parameter style conversion based on dialect support
77
+ sql, params = statement.compile(placeholder_style=self.default_parameter_style)
78
+
79
+ if statement.is_many:
80
+ # Process parameter list through type coercion
81
+ params = self._process_parameters(params)
82
+ return await self._execute_many(sql, params, connection=connection, **kwargs)
83
+
84
+ # Process parameters through type coercion
85
+ params = self._process_parameters(params)
86
+ return await self._execute(sql, params, statement, connection=connection, **kwargs)
87
+
88
+ async def _execute(
89
+ self, sql: str, parameters: Any, statement: SQL, connection: "Optional[AsyncmyConnection]" = None, **kwargs: Any
90
+ ) -> Union[SelectResultDict, DMLResultDict]:
91
+ conn = self._connection(connection)
92
+ # AsyncMy doesn't like empty lists/tuples, convert to None
93
+ if not parameters:
94
+ parameters = None
95
+ async with self._get_cursor(conn) as cursor:
96
+ # AsyncMy expects list/tuple parameters or dict for named params
270
97
  await cursor.execute(sql, parameters)
271
- result = await cursor.fetchone()
272
- if result is None:
273
- return None
274
- column_names = [c[0] for c in cursor.description or []]
275
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
276
-
277
- @overload
278
- async def select_value(
279
- self,
280
- sql: str,
281
- parameters: "Optional[StatementParameterType]" = None,
282
- *filters: "StatementFilter",
283
- connection: "Optional[AsyncmyConnection]" = None,
284
- schema_type: None = None,
285
- **kwargs: Any,
286
- ) -> "Any": ...
287
- @overload
288
- async def select_value(
289
- self,
290
- sql: str,
291
- parameters: "Optional[StatementParameterType]" = None,
292
- *filters: "StatementFilter",
293
- connection: "Optional[AsyncmyConnection]" = None,
294
- schema_type: "type[T]",
295
- **kwargs: Any,
296
- ) -> "T": ...
297
- async def select_value(
298
- self,
299
- sql: str,
300
- parameters: "Optional[StatementParameterType]" = None,
301
- *filters: "StatementFilter",
302
- connection: "Optional[AsyncmyConnection]" = None,
303
- schema_type: "Optional[type[T]]" = None,
304
- **kwargs: Any,
305
- ) -> "Union[T, Any]":
306
- """Fetch a single value from the database.
307
-
308
- Returns:
309
- The first value from the first row of results.
310
- """
311
- connection = self._connection(connection)
312
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
313
- async with self._with_cursor(connection) as cursor:
314
- await cursor.execute(sql, parameters)
315
- result = await cursor.fetchone()
316
- result = self.check_not_found(result)
317
- value = result[0]
318
- if schema_type is not None:
319
- return schema_type(value) # type: ignore[call-arg]
320
- return value
321
-
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[AsyncmyConnection]" = None,
329
- schema_type: None = None,
330
- **kwargs: Any,
331
- ) -> "Optional[Any]": ...
332
- @overload
333
- async def select_value_or_none(
334
- self,
335
- sql: str,
336
- parameters: "Optional[StatementParameterType]" = None,
337
- *filters: "StatementFilter",
338
- connection: "Optional[AsyncmyConnection]" = None,
339
- schema_type: "type[T]",
340
- **kwargs: Any,
341
- ) -> "Optional[T]": ...
342
- async def select_value_or_none(
343
- self,
344
- sql: str,
345
- parameters: "Optional[StatementParameterType]" = None,
346
- *filters: "StatementFilter",
347
- connection: "Optional[AsyncmyConnection]" = None,
348
- schema_type: "Optional[type[T]]" = None,
349
- **kwargs: Any,
350
- ) -> "Optional[Union[T, Any]]":
351
- """Fetch a single value from the database.
352
-
353
- Returns:
354
- The first value from the first row of results, or None if no results.
355
- """
356
- connection = self._connection(connection)
357
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
358
- async with self._with_cursor(connection) as cursor:
359
- await cursor.execute(sql, parameters)
360
- result = await cursor.fetchone()
361
- if result is None:
362
- return None
363
- value = result[0]
364
- if schema_type is not None:
365
- return schema_type(value) # type: ignore[call-arg]
366
- return value
367
-
368
- async def insert_update_delete(
369
- self,
370
- sql: str,
371
- parameters: "Optional[StatementParameterType]" = None,
372
- *filters: "StatementFilter",
373
- connection: "Optional[AsyncmyConnection]" = None,
374
- **kwargs: Any,
375
- ) -> int:
376
- """Insert, update, or delete data from the database.
377
-
378
- Returns:
379
- Row count affected by the operation.
380
- """
381
- connection = self._connection(connection)
382
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
383
- async with self._with_cursor(connection) as cursor:
384
- await cursor.execute(sql, parameters)
385
- return cursor.rowcount
386
-
387
- @overload
388
- async def insert_update_delete_returning(
389
- self,
390
- sql: str,
391
- parameters: "Optional[StatementParameterType]" = None,
392
- *filters: "StatementFilter",
393
- connection: "Optional[AsyncmyConnection]" = None,
394
- schema_type: None = None,
395
- **kwargs: Any,
396
- ) -> "dict[str, Any]": ...
397
- @overload
398
- async def insert_update_delete_returning(
399
- self,
400
- sql: str,
401
- parameters: "Optional[StatementParameterType]" = None,
402
- *filters: "StatementFilter",
403
- connection: "Optional[AsyncmyConnection]" = None,
404
- schema_type: "type[ModelDTOT]",
405
- **kwargs: Any,
406
- ) -> "ModelDTOT": ...
407
- async def insert_update_delete_returning(
408
- self,
409
- sql: str,
410
- parameters: "Optional[StatementParameterType]" = None,
411
- *filters: "StatementFilter",
412
- connection: "Optional[AsyncmyConnection]" = None,
413
- schema_type: "Optional[type[ModelDTOT]]" = None,
414
- **kwargs: Any,
415
- ) -> "Union[dict[str, Any], ModelDTOT]":
416
- """Insert, update, or delete data from the database and return result.
417
-
418
- Returns:
419
- The first row of results.
420
- """
421
- connection = self._connection(connection)
422
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
423
-
424
- async with self._with_cursor(connection) as cursor:
425
- await cursor.execute(sql, parameters)
426
- result = await cursor.fetchone()
427
- if result is None:
428
- return None
429
- column_names = [c[0] for c in cursor.description or []]
430
-
431
- # Convert to dict and use ResultConverter
432
- dict_result = dict(zip(column_names, result))
433
- return self.to_schema(dict_result, schema_type=schema_type)
434
-
435
- async def execute_script(
436
- self,
437
- sql: str,
438
- parameters: "Optional[StatementParameterType]" = None,
439
- connection: "Optional[AsyncmyConnection]" = None,
440
- **kwargs: Any,
441
- ) -> str:
442
- """Execute a script.
443
-
444
- Returns:
445
- Status message for the operation.
446
- """
447
- connection = self._connection(connection)
448
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
449
- async with self._with_cursor(connection) as cursor:
450
- await cursor.execute(sql, parameters)
451
- return f"Script executed successfully. Rows affected: {cursor.rowcount}"
452
-
453
- def _connection(self, connection: "Optional[AsyncmyConnection]" = None) -> "AsyncmyConnection":
454
- """Get the connection to use for the operation.
455
-
456
- Args:
457
- connection: Optional connection to use.
458
98
 
459
- Returns:
460
- The connection to use.
461
- """
99
+ if self.returns_rows(statement.expression):
100
+ # For SELECT queries, fetch data and return SelectResultDict
101
+ data = await cursor.fetchall()
102
+ column_names = [desc[0] for desc in cursor.description or []]
103
+ result: SelectResultDict = {"data": data, "column_names": column_names, "rows_affected": len(data)}
104
+ return result
105
+
106
+ # For DML/DDL queries, return DMLResultDict
107
+ dml_result: DMLResultDict = {
108
+ "rows_affected": cursor.rowcount if cursor.rowcount is not None else -1,
109
+ "status_message": "OK",
110
+ }
111
+ return dml_result
112
+
113
+ async def _execute_many(
114
+ self, sql: str, param_list: Any, connection: "Optional[AsyncmyConnection]" = None, **kwargs: Any
115
+ ) -> DMLResultDict:
116
+ conn = self._connection(connection)
117
+
118
+ # Convert parameter list to proper format for executemany
119
+ params_list: list[Union[list[Any], tuple[Any, ...]]] = []
120
+ if param_list and isinstance(param_list, Sequence):
121
+ for param_set in param_list:
122
+ if isinstance(param_set, (list, tuple)):
123
+ params_list.append(param_set)
124
+ elif param_set is None:
125
+ params_list.append([])
126
+ else:
127
+ params_list.append([param_set])
128
+
129
+ async with self._get_cursor(conn) as cursor:
130
+ await cursor.executemany(sql, params_list)
131
+ result: DMLResultDict = {
132
+ "rows_affected": cursor.rowcount if cursor.rowcount != -1 else len(params_list),
133
+ "status_message": "OK",
134
+ }
135
+ return result
136
+
137
+ async def _execute_script(
138
+ self, script: str, connection: "Optional[AsyncmyConnection]" = None, **kwargs: Any
139
+ ) -> ScriptResultDict:
140
+ conn = self._connection(connection)
141
+ # AsyncMy may not support multi-statement scripts without CLIENT_MULTI_STATEMENTS flag
142
+ # Use the shared implementation to split and execute statements individually
143
+ statements = self._split_script_statements(script)
144
+ statements_executed = 0
145
+
146
+ async with self._get_cursor(conn) as cursor:
147
+ for statement_str in statements:
148
+ if statement_str:
149
+ await cursor.execute(statement_str)
150
+ statements_executed += 1
151
+
152
+ result: ScriptResultDict = {"statements_executed": statements_executed, "status_message": "SCRIPT EXECUTED"}
153
+ return result
154
+
155
+ async def _ingest_arrow_table(self, table: "Any", table_name: str, mode: str = "append", **options: Any) -> int:
156
+ self._ensure_pyarrow_installed()
157
+ conn = self._connection(None)
158
+
159
+ async with self._get_cursor(conn) as cursor:
160
+ if mode == "replace":
161
+ await cursor.execute(f"TRUNCATE TABLE {table_name}")
162
+ elif mode == "create":
163
+ msg = "'create' mode is not supported for asyncmy ingestion."
164
+ raise NotImplementedError(msg)
165
+
166
+ data_for_ingest = table.to_pylist()
167
+ if not data_for_ingest:
168
+ return 0
169
+
170
+ # Generate column placeholders: %s, %s, etc.
171
+ num_columns = len(data_for_ingest[0])
172
+ placeholders = ", ".join("%s" for _ in range(num_columns))
173
+ sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
174
+ await cursor.executemany(sql, data_for_ingest)
175
+ return cursor.rowcount if cursor.rowcount is not None else -1
176
+
177
+ async def _wrap_select_result(
178
+ self, statement: SQL, result: SelectResultDict, schema_type: "Optional[type[ModelDTOT]]" = None, **kwargs: Any
179
+ ) -> "Union[SQLResult[ModelDTOT], SQLResult[RowT]]":
180
+ data = result["data"]
181
+ column_names = result["column_names"]
182
+ rows_affected = result["rows_affected"]
183
+
184
+ if not data:
185
+ return SQLResult[RowT](
186
+ statement=statement, data=[], column_names=column_names, rows_affected=0, operation_type="SELECT"
187
+ )
188
+
189
+ rows_as_dicts = [dict(zip(column_names, row)) for row in data]
190
+
191
+ if schema_type:
192
+ converted_data = self.to_schema(data=rows_as_dicts, schema_type=schema_type)
193
+ return SQLResult[ModelDTOT](
194
+ statement=statement,
195
+ data=list(converted_data),
196
+ column_names=column_names,
197
+ rows_affected=rows_affected,
198
+ operation_type="SELECT",
199
+ )
200
+
201
+ return SQLResult[RowT](
202
+ statement=statement,
203
+ data=rows_as_dicts,
204
+ column_names=column_names,
205
+ rows_affected=rows_affected,
206
+ operation_type="SELECT",
207
+ )
208
+
209
+ async def _wrap_execute_result(
210
+ self, statement: SQL, result: Union[DMLResultDict, ScriptResultDict], **kwargs: Any
211
+ ) -> SQLResult[RowT]:
212
+ operation_type = "UNKNOWN"
213
+ if statement.expression:
214
+ operation_type = str(statement.expression.key).upper()
215
+
216
+ # Handle script results
217
+ if "statements_executed" in result:
218
+ script_result = cast("ScriptResultDict", result)
219
+ return SQLResult[RowT](
220
+ statement=statement,
221
+ data=[],
222
+ rows_affected=0,
223
+ operation_type="SCRIPT",
224
+ metadata={
225
+ "status_message": script_result.get("status_message", ""),
226
+ "statements_executed": script_result.get("statements_executed", -1),
227
+ },
228
+ )
229
+
230
+ # Handle DML results
231
+ dml_result = cast("DMLResultDict", result)
232
+ rows_affected = dml_result.get("rows_affected", -1)
233
+ status_message = dml_result.get("status_message", "")
234
+ return SQLResult[RowT](
235
+ statement=statement,
236
+ data=[],
237
+ rows_affected=rows_affected,
238
+ operation_type=operation_type,
239
+ metadata={"status_message": status_message},
240
+ )
241
+
242
+ def _connection(self, connection: Optional["AsyncmyConnection"] = None) -> "AsyncmyConnection":
243
+ """Get the connection to use for the operation."""
462
244
  return connection or self.connection