sqlspec 0.10.1__py3-none-any.whl → 0.11.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.

@@ -1,14 +1,22 @@
1
1
  # type: ignore
2
- from collections.abc import AsyncGenerator, Sequence
2
+ import logging
3
+ import re
4
+ from collections.abc import AsyncGenerator
3
5
  from contextlib import asynccontextmanager
4
- from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
6
+ from typing import TYPE_CHECKING, Any, Optional, Union, overload
5
7
 
6
8
  from asyncmy import Connection
7
9
 
8
10
  from sqlspec.base import AsyncDriverAdapterProtocol
9
- from sqlspec.mixins import SQLTranslatorMixin
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
10
16
 
11
17
  if TYPE_CHECKING:
18
+ from collections.abc import Mapping, Sequence
19
+
12
20
  from asyncmy.cursors import Cursor
13
21
 
14
22
  from sqlspec.typing import ModelDTOT, StatementParameterType, T
@@ -17,10 +25,16 @@ __all__ = ("AsyncmyDriver",)
17
25
 
18
26
  AsyncmyConnection = Connection
19
27
 
28
+ logger = logging.getLogger("sqlspec")
29
+
30
+ # Pattern to identify MySQL-style placeholders (%s) for proper conversion
31
+ MYSQL_PLACEHOLDER_PATTERN = re.compile(r"(?<!%)%s")
32
+
20
33
 
21
34
  class AsyncmyDriver(
22
35
  SQLTranslatorMixin["AsyncmyConnection"],
23
36
  AsyncDriverAdapterProtocol["AsyncmyConnection"],
37
+ ResultConverter,
24
38
  ):
25
39
  """Asyncmy MySQL/MariaDB Driver Adapter."""
26
40
 
@@ -39,14 +53,102 @@ class AsyncmyDriver(
39
53
  finally:
40
54
  await cursor.close()
41
55
 
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
+
42
145
  # --- Public API Methods --- #
43
146
  @overload
44
147
  async def select(
45
148
  self,
46
149
  sql: str,
47
150
  parameters: "Optional[StatementParameterType]" = None,
48
- /,
49
- *,
151
+ *filters: "StatementFilter",
50
152
  connection: "Optional[AsyncmyConnection]" = None,
51
153
  schema_type: None = None,
52
154
  **kwargs: Any,
@@ -56,8 +158,7 @@ class AsyncmyDriver(
56
158
  self,
57
159
  sql: str,
58
160
  parameters: "Optional[StatementParameterType]" = None,
59
- /,
60
- *,
161
+ *filters: "StatementFilter",
61
162
  connection: "Optional[AsyncmyConnection]" = None,
62
163
  schema_type: "type[ModelDTOT]",
63
164
  **kwargs: Any,
@@ -65,37 +166,33 @@ class AsyncmyDriver(
65
166
  async def select(
66
167
  self,
67
168
  sql: str,
68
- parameters: Optional["StatementParameterType"] = None,
69
- /,
70
- *,
71
- connection: Optional["AsyncmyConnection"] = None,
169
+ parameters: "Optional[StatementParameterType]" = None,
170
+ *filters: "StatementFilter",
171
+ connection: "Optional[AsyncmyConnection]" = None,
72
172
  schema_type: "Optional[type[ModelDTOT]]" = None,
73
173
  **kwargs: Any,
74
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
174
+ ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
75
175
  """Fetch data from the database.
76
176
 
77
177
  Returns:
78
178
  List of row data as either model instances or dictionaries.
79
179
  """
80
180
  connection = self._connection(connection)
81
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
181
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
82
182
  async with self._with_cursor(connection) as cursor:
83
183
  await cursor.execute(sql, parameters)
84
184
  results = await cursor.fetchall()
85
185
  if not results:
86
186
  return []
87
187
  column_names = [c[0] for c in cursor.description or []]
88
- if schema_type is None:
89
- return [dict(zip(column_names, row)) for row in results]
90
- return [schema_type(**dict(zip(column_names, row))) for row in results]
188
+ return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
91
189
 
92
190
  @overload
93
191
  async def select_one(
94
192
  self,
95
193
  sql: str,
96
194
  parameters: "Optional[StatementParameterType]" = None,
97
- /,
98
- *,
195
+ *filters: "StatementFilter",
99
196
  connection: "Optional[AsyncmyConnection]" = None,
100
197
  schema_type: None = None,
101
198
  **kwargs: Any,
@@ -105,8 +202,7 @@ class AsyncmyDriver(
105
202
  self,
106
203
  sql: str,
107
204
  parameters: "Optional[StatementParameterType]" = None,
108
- /,
109
- *,
205
+ *filters: "StatementFilter",
110
206
  connection: "Optional[AsyncmyConnection]" = None,
111
207
  schema_type: "type[ModelDTOT]",
112
208
  **kwargs: Any,
@@ -114,36 +210,32 @@ class AsyncmyDriver(
114
210
  async def select_one(
115
211
  self,
116
212
  sql: str,
117
- parameters: Optional["StatementParameterType"] = None,
118
- /,
119
- *,
120
- connection: Optional["AsyncmyConnection"] = None,
213
+ parameters: "Optional[StatementParameterType]" = None,
214
+ *filters: "StatementFilter",
215
+ connection: "Optional[AsyncmyConnection]" = None,
121
216
  schema_type: "Optional[type[ModelDTOT]]" = None,
122
217
  **kwargs: Any,
123
- ) -> "Union[ModelDTOT, dict[str, Any]]":
218
+ ) -> "Union[dict[str, Any], ModelDTOT]":
124
219
  """Fetch one row from the database.
125
220
 
126
221
  Returns:
127
222
  The first row of the query results.
128
223
  """
129
224
  connection = self._connection(connection)
130
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
225
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
131
226
  async with self._with_cursor(connection) as cursor:
132
227
  await cursor.execute(sql, parameters)
133
228
  result = await cursor.fetchone()
134
229
  result = self.check_not_found(result)
135
230
  column_names = [c[0] for c in cursor.description or []]
136
- if schema_type is None:
137
- return dict(zip(column_names, result))
138
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result))))
231
+ return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
139
232
 
140
233
  @overload
141
234
  async def select_one_or_none(
142
235
  self,
143
236
  sql: str,
144
237
  parameters: "Optional[StatementParameterType]" = None,
145
- /,
146
- *,
238
+ *filters: "StatementFilter",
147
239
  connection: "Optional[AsyncmyConnection]" = None,
148
240
  schema_type: None = None,
149
241
  **kwargs: Any,
@@ -153,8 +245,7 @@ class AsyncmyDriver(
153
245
  self,
154
246
  sql: str,
155
247
  parameters: "Optional[StatementParameterType]" = None,
156
- /,
157
- *,
248
+ *filters: "StatementFilter",
158
249
  connection: "Optional[AsyncmyConnection]" = None,
159
250
  schema_type: "type[ModelDTOT]",
160
251
  **kwargs: Any,
@@ -162,37 +253,33 @@ class AsyncmyDriver(
162
253
  async def select_one_or_none(
163
254
  self,
164
255
  sql: str,
165
- parameters: Optional["StatementParameterType"] = None,
166
- /,
167
- *,
168
- connection: Optional["AsyncmyConnection"] = None,
256
+ parameters: "Optional[StatementParameterType]" = None,
257
+ *filters: "StatementFilter",
258
+ connection: "Optional[AsyncmyConnection]" = None,
169
259
  schema_type: "Optional[type[ModelDTOT]]" = None,
170
260
  **kwargs: Any,
171
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
261
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
172
262
  """Fetch one row from the database.
173
263
 
174
264
  Returns:
175
265
  The first row of the query results.
176
266
  """
177
267
  connection = self._connection(connection)
178
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
268
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
179
269
  async with self._with_cursor(connection) as cursor:
180
270
  await cursor.execute(sql, parameters)
181
271
  result = await cursor.fetchone()
182
272
  if result is None:
183
273
  return None
184
274
  column_names = [c[0] for c in cursor.description or []]
185
- if schema_type is None:
186
- return dict(zip(column_names, result))
187
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result))))
275
+ return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
188
276
 
189
277
  @overload
190
278
  async def select_value(
191
279
  self,
192
280
  sql: str,
193
281
  parameters: "Optional[StatementParameterType]" = None,
194
- /,
195
- *,
282
+ *filters: "StatementFilter",
196
283
  connection: "Optional[AsyncmyConnection]" = None,
197
284
  schema_type: None = None,
198
285
  **kwargs: Any,
@@ -202,8 +289,7 @@ class AsyncmyDriver(
202
289
  self,
203
290
  sql: str,
204
291
  parameters: "Optional[StatementParameterType]" = None,
205
- /,
206
- *,
292
+ *filters: "StatementFilter",
207
293
  connection: "Optional[AsyncmyConnection]" = None,
208
294
  schema_type: "type[T]",
209
295
  **kwargs: Any,
@@ -212,8 +298,7 @@ class AsyncmyDriver(
212
298
  self,
213
299
  sql: str,
214
300
  parameters: "Optional[StatementParameterType]" = None,
215
- /,
216
- *,
301
+ *filters: "StatementFilter",
217
302
  connection: "Optional[AsyncmyConnection]" = None,
218
303
  schema_type: "Optional[type[T]]" = None,
219
304
  **kwargs: Any,
@@ -221,16 +306,14 @@ class AsyncmyDriver(
221
306
  """Fetch a single value from the database.
222
307
 
223
308
  Returns:
224
- The first value from the first row of results, or None if no results.
309
+ The first value from the first row of results.
225
310
  """
226
311
  connection = self._connection(connection)
227
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
228
-
312
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
229
313
  async with self._with_cursor(connection) as cursor:
230
314
  await cursor.execute(sql, parameters)
231
315
  result = await cursor.fetchone()
232
316
  result = self.check_not_found(result)
233
-
234
317
  value = result[0]
235
318
  if schema_type is not None:
236
319
  return schema_type(value) # type: ignore[call-arg]
@@ -241,8 +324,7 @@ class AsyncmyDriver(
241
324
  self,
242
325
  sql: str,
243
326
  parameters: "Optional[StatementParameterType]" = None,
244
- /,
245
- *,
327
+ *filters: "StatementFilter",
246
328
  connection: "Optional[AsyncmyConnection]" = None,
247
329
  schema_type: None = None,
248
330
  **kwargs: Any,
@@ -252,8 +334,7 @@ class AsyncmyDriver(
252
334
  self,
253
335
  sql: str,
254
336
  parameters: "Optional[StatementParameterType]" = None,
255
- /,
256
- *,
337
+ *filters: "StatementFilter",
257
338
  connection: "Optional[AsyncmyConnection]" = None,
258
339
  schema_type: "type[T]",
259
340
  **kwargs: Any,
@@ -262,8 +343,7 @@ class AsyncmyDriver(
262
343
  self,
263
344
  sql: str,
264
345
  parameters: "Optional[StatementParameterType]" = None,
265
- /,
266
- *,
346
+ *filters: "StatementFilter",
267
347
  connection: "Optional[AsyncmyConnection]" = None,
268
348
  schema_type: "Optional[type[T]]" = None,
269
349
  **kwargs: Any,
@@ -274,15 +354,12 @@ class AsyncmyDriver(
274
354
  The first value from the first row of results, or None if no results.
275
355
  """
276
356
  connection = self._connection(connection)
277
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
278
-
357
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
279
358
  async with self._with_cursor(connection) as cursor:
280
359
  await cursor.execute(sql, parameters)
281
360
  result = await cursor.fetchone()
282
-
283
361
  if result is None:
284
362
  return None
285
-
286
363
  value = result[0]
287
364
  if schema_type is not None:
288
365
  return schema_type(value) # type: ignore[call-arg]
@@ -291,10 +368,9 @@ class AsyncmyDriver(
291
368
  async def insert_update_delete(
292
369
  self,
293
370
  sql: str,
294
- parameters: Optional["StatementParameterType"] = None,
295
- /,
296
- *,
297
- connection: Optional["AsyncmyConnection"] = None,
371
+ parameters: "Optional[StatementParameterType]" = None,
372
+ *filters: "StatementFilter",
373
+ connection: "Optional[AsyncmyConnection]" = None,
298
374
  **kwargs: Any,
299
375
  ) -> int:
300
376
  """Insert, update, or delete data from the database.
@@ -303,8 +379,7 @@ class AsyncmyDriver(
303
379
  Row count affected by the operation.
304
380
  """
305
381
  connection = self._connection(connection)
306
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
307
-
382
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
308
383
  async with self._with_cursor(connection) as cursor:
309
384
  await cursor.execute(sql, parameters)
310
385
  return cursor.rowcount
@@ -314,8 +389,7 @@ class AsyncmyDriver(
314
389
  self,
315
390
  sql: str,
316
391
  parameters: "Optional[StatementParameterType]" = None,
317
- /,
318
- *,
392
+ *filters: "StatementFilter",
319
393
  connection: "Optional[AsyncmyConnection]" = None,
320
394
  schema_type: None = None,
321
395
  **kwargs: Any,
@@ -325,8 +399,7 @@ class AsyncmyDriver(
325
399
  self,
326
400
  sql: str,
327
401
  parameters: "Optional[StatementParameterType]" = None,
328
- /,
329
- *,
402
+ *filters: "StatementFilter",
330
403
  connection: "Optional[AsyncmyConnection]" = None,
331
404
  schema_type: "type[ModelDTOT]",
332
405
  **kwargs: Any,
@@ -334,21 +407,19 @@ class AsyncmyDriver(
334
407
  async def insert_update_delete_returning(
335
408
  self,
336
409
  sql: str,
337
- parameters: Optional["StatementParameterType"] = None,
338
- /,
339
- *,
340
- connection: Optional["AsyncmyConnection"] = None,
410
+ parameters: "Optional[StatementParameterType]" = None,
411
+ *filters: "StatementFilter",
412
+ connection: "Optional[AsyncmyConnection]" = None,
341
413
  schema_type: "Optional[type[ModelDTOT]]" = None,
342
414
  **kwargs: Any,
343
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
415
+ ) -> "Union[dict[str, Any], ModelDTOT]":
344
416
  """Insert, update, or delete data from the database and return result.
345
417
 
346
418
  Returns:
347
419
  The first row of results.
348
420
  """
349
421
  connection = self._connection(connection)
350
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
351
- column_names: list[str] = []
422
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
352
423
 
353
424
  async with self._with_cursor(connection) as cursor:
354
425
  await cursor.execute(sql, parameters)
@@ -356,17 +427,16 @@ class AsyncmyDriver(
356
427
  if result is None:
357
428
  return None
358
429
  column_names = [c[0] for c in cursor.description or []]
359
- if schema_type is not None:
360
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result))))
361
- return dict(zip(column_names, result))
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)
362
434
 
363
435
  async def execute_script(
364
436
  self,
365
437
  sql: str,
366
- parameters: Optional["StatementParameterType"] = None,
367
- /,
368
- *,
369
- connection: Optional["AsyncmyConnection"] = None,
438
+ parameters: "Optional[StatementParameterType]" = None,
439
+ connection: "Optional[AsyncmyConnection]" = None,
370
440
  **kwargs: Any,
371
441
  ) -> str:
372
442
  """Execute a script.
@@ -376,7 +446,17 @@ class AsyncmyDriver(
376
446
  """
377
447
  connection = self._connection(connection)
378
448
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
379
-
380
449
  async with self._with_cursor(connection) as cursor:
381
450
  await cursor.execute(sql, parameters)
382
- return "DONE"
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
+
459
+ Returns:
460
+ The connection to use.
461
+ """
462
+ return connection or self.connection
@@ -83,7 +83,9 @@ class AsyncpgConfig(AsyncDatabaseConfig["AsyncpgConnection", "Pool", "AsyncpgDri
83
83
  """For dialects that support the JSON datatype, this is a Python callable that will render a given object as JSON.
84
84
  By default, SQLSpec's :attr:`encode_json() <sqlspec._serialization.encode_json>` is used."""
85
85
  connection_type: "type[AsyncpgConnection]" = field(
86
- hash=False, init=False, default_factory=lambda: PoolConnectionProxy
86
+ hash=False,
87
+ init=False,
88
+ default_factory=lambda: PoolConnectionProxy, # type: ignore[assignment]
87
89
  )
88
90
  """Type of the connection object"""
89
91
  driver_type: "type[AsyncpgDriver]" = field(hash=False, init=False, default_factory=lambda: AsyncpgDriver) # type: ignore[type-abstract,unused-ignore]