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

@@ -1,26 +1,38 @@
1
1
  # type: ignore
2
+ import logging
3
+ import re
2
4
  from collections.abc import AsyncGenerator, Sequence
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.mixins import ResultConverter, SQLTranslatorMixin
13
+ from sqlspec.statement import SQLStatement
14
+ from sqlspec.typing import is_dict
10
15
 
11
16
  if TYPE_CHECKING:
12
17
  from asyncmy.cursors import Cursor
13
18
 
19
+ from sqlspec.filters import StatementFilter
14
20
  from sqlspec.typing import ModelDTOT, StatementParameterType, T
15
21
 
16
22
  __all__ = ("AsyncmyDriver",)
17
23
 
18
24
  AsyncmyConnection = Connection
19
25
 
26
+ logger = logging.getLogger("sqlspec")
27
+
28
+ # Pattern to identify MySQL-style placeholders (%s) for proper conversion
29
+ MYSQL_PLACEHOLDER_PATTERN = re.compile(r"(?<!%)%s")
30
+
20
31
 
21
32
  class AsyncmyDriver(
22
33
  SQLTranslatorMixin["AsyncmyConnection"],
23
34
  AsyncDriverAdapterProtocol["AsyncmyConnection"],
35
+ ResultConverter,
24
36
  ):
25
37
  """Asyncmy MySQL/MariaDB Driver Adapter."""
26
38
 
@@ -39,6 +51,85 @@ class AsyncmyDriver(
39
51
  finally:
40
52
  await cursor.close()
41
53
 
54
+ def _process_sql_params(
55
+ self,
56
+ sql: str,
57
+ parameters: "Optional[StatementParameterType]" = None,
58
+ /,
59
+ *filters: "StatementFilter",
60
+ **kwargs: Any,
61
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
62
+ """Process SQL and parameters using SQLStatement with dialect support.
63
+
64
+ Args:
65
+ sql: The SQL statement to process.
66
+ parameters: The parameters to bind to the statement.
67
+ *filters: Statement filters to apply.
68
+ **kwargs: Additional keyword arguments.
69
+
70
+ Raises:
71
+ ParameterStyleMismatchError: If the parameter style is not supported.
72
+
73
+ Returns:
74
+ A tuple of (sql, parameters) ready for execution.
75
+ """
76
+ # Handle MySQL-specific placeholders (%s) which SQLGlot doesn't parse well
77
+ # If %s placeholders are present, handle them directly
78
+ mysql_placeholders_count = len(MYSQL_PLACEHOLDER_PATTERN.findall(sql))
79
+
80
+ if mysql_placeholders_count > 0:
81
+ # For MySQL format placeholders, minimal processing is needed
82
+ if parameters is None:
83
+ if mysql_placeholders_count > 0:
84
+ msg = f"asyncmy: SQL statement contains {mysql_placeholders_count} format placeholders ('%s'), but no parameters were provided. SQL: {sql}"
85
+ raise ParameterStyleMismatchError(msg)
86
+ return sql, None
87
+
88
+ # Convert dict to tuple if needed
89
+ if is_dict(parameters):
90
+ # MySQL's %s placeholders require positional params
91
+ msg = "asyncmy: Dictionary parameters provided with '%s' placeholders. MySQL format placeholders require tuple/list parameters."
92
+ raise ParameterStyleMismatchError(msg)
93
+
94
+ # Convert to tuple (handles both scalar and sequence cases)
95
+ if not isinstance(parameters, (list, tuple)):
96
+ # Scalar parameter case
97
+ return sql, (parameters,)
98
+
99
+ # Sequence parameter case - ensure appropriate length
100
+ if len(parameters) != mysql_placeholders_count:
101
+ msg = f"asyncmy: Parameter count mismatch. SQL expects {mysql_placeholders_count} '%s' placeholders, but {len(parameters)} parameters were provided. SQL: {sql}"
102
+ raise ParameterStyleMismatchError(msg)
103
+
104
+ return sql, tuple(parameters)
105
+
106
+ # Create a SQLStatement with MySQL dialect
107
+ statement = SQLStatement(sql, parameters, kwargs=kwargs, dialect=self.dialect)
108
+
109
+ # Apply any filters
110
+ for filter_obj in filters:
111
+ statement = statement.apply_filter(filter_obj)
112
+
113
+ # Process the statement for execution
114
+ processed_sql, processed_params, _ = statement.process()
115
+
116
+ # Convert parameters to the format expected by MySQL
117
+ if processed_params is None:
118
+ return processed_sql, None
119
+
120
+ # For MySQL, ensure parameters are in the right format
121
+ if is_dict(processed_params):
122
+ # Dictionary parameters are not well supported by asyncmy
123
+ msg = "asyncmy: Dictionary parameters are not supported for MySQL placeholders. Use sequence parameters."
124
+ raise ParameterStyleMismatchError(msg)
125
+
126
+ # For sequence parameters, ensure they're a tuple
127
+ if isinstance(processed_params, (list, tuple)):
128
+ return processed_sql, tuple(processed_params)
129
+
130
+ # For scalar parameter, wrap in a tuple
131
+ return processed_sql, (processed_params,)
132
+
42
133
  # --- Public API Methods --- #
43
134
  @overload
44
135
  async def select(
@@ -46,7 +137,7 @@ class AsyncmyDriver(
46
137
  sql: str,
47
138
  parameters: "Optional[StatementParameterType]" = None,
48
139
  /,
49
- *,
140
+ *filters: "StatementFilter",
50
141
  connection: "Optional[AsyncmyConnection]" = None,
51
142
  schema_type: None = None,
52
143
  **kwargs: Any,
@@ -57,7 +148,7 @@ class AsyncmyDriver(
57
148
  sql: str,
58
149
  parameters: "Optional[StatementParameterType]" = None,
59
150
  /,
60
- *,
151
+ *filters: "StatementFilter",
61
152
  connection: "Optional[AsyncmyConnection]" = None,
62
153
  schema_type: "type[ModelDTOT]",
63
154
  **kwargs: Any,
@@ -65,29 +156,30 @@ class AsyncmyDriver(
65
156
  async def select(
66
157
  self,
67
158
  sql: str,
68
- parameters: Optional["StatementParameterType"] = None,
159
+ parameters: "Optional[StatementParameterType]" = None,
69
160
  /,
70
- *,
71
- connection: Optional["AsyncmyConnection"] = None,
161
+ *filters: "StatementFilter",
162
+ connection: "Optional[AsyncmyConnection]" = None,
72
163
  schema_type: "Optional[type[ModelDTOT]]" = None,
73
164
  **kwargs: Any,
74
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
165
+ ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
75
166
  """Fetch data from the database.
76
167
 
77
168
  Returns:
78
169
  List of row data as either model instances or dictionaries.
79
170
  """
80
171
  connection = self._connection(connection)
81
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
172
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
82
173
  async with self._with_cursor(connection) as cursor:
83
174
  await cursor.execute(sql, parameters)
84
175
  results = await cursor.fetchall()
85
176
  if not results:
86
177
  return []
87
178
  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]
179
+
180
+ # Convert to dicts first
181
+ dict_results = [dict(zip(column_names, row)) for row in results]
182
+ return self.to_schema(dict_results, schema_type=schema_type)
91
183
 
92
184
  @overload
93
185
  async def select_one(
@@ -95,7 +187,7 @@ class AsyncmyDriver(
95
187
  sql: str,
96
188
  parameters: "Optional[StatementParameterType]" = None,
97
189
  /,
98
- *,
190
+ *filters: "StatementFilter",
99
191
  connection: "Optional[AsyncmyConnection]" = None,
100
192
  schema_type: None = None,
101
193
  **kwargs: Any,
@@ -106,7 +198,7 @@ class AsyncmyDriver(
106
198
  sql: str,
107
199
  parameters: "Optional[StatementParameterType]" = None,
108
200
  /,
109
- *,
201
+ *filters: "StatementFilter",
110
202
  connection: "Optional[AsyncmyConnection]" = None,
111
203
  schema_type: "type[ModelDTOT]",
112
204
  **kwargs: Any,
@@ -114,28 +206,29 @@ class AsyncmyDriver(
114
206
  async def select_one(
115
207
  self,
116
208
  sql: str,
117
- parameters: Optional["StatementParameterType"] = None,
209
+ parameters: "Optional[StatementParameterType]" = None,
118
210
  /,
119
- *,
120
- connection: Optional["AsyncmyConnection"] = None,
211
+ *filters: "StatementFilter",
212
+ connection: "Optional[AsyncmyConnection]" = None,
121
213
  schema_type: "Optional[type[ModelDTOT]]" = None,
122
214
  **kwargs: Any,
123
- ) -> "Union[ModelDTOT, dict[str, Any]]":
215
+ ) -> "Union[dict[str, Any], ModelDTOT]":
124
216
  """Fetch one row from the database.
125
217
 
126
218
  Returns:
127
219
  The first row of the query results.
128
220
  """
129
221
  connection = self._connection(connection)
130
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
222
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
131
223
  async with self._with_cursor(connection) as cursor:
132
224
  await cursor.execute(sql, parameters)
133
225
  result = await cursor.fetchone()
134
226
  result = self.check_not_found(result)
135
227
  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))))
228
+
229
+ # Convert to dict and use ResultConverter
230
+ dict_result = dict(zip(column_names, result))
231
+ return self.to_schema(dict_result, schema_type=schema_type)
139
232
 
140
233
  @overload
141
234
  async def select_one_or_none(
@@ -143,7 +236,7 @@ class AsyncmyDriver(
143
236
  sql: str,
144
237
  parameters: "Optional[StatementParameterType]" = None,
145
238
  /,
146
- *,
239
+ *filters: "StatementFilter",
147
240
  connection: "Optional[AsyncmyConnection]" = None,
148
241
  schema_type: None = None,
149
242
  **kwargs: Any,
@@ -154,7 +247,7 @@ class AsyncmyDriver(
154
247
  sql: str,
155
248
  parameters: "Optional[StatementParameterType]" = None,
156
249
  /,
157
- *,
250
+ *filters: "StatementFilter",
158
251
  connection: "Optional[AsyncmyConnection]" = None,
159
252
  schema_type: "type[ModelDTOT]",
160
253
  **kwargs: Any,
@@ -162,29 +255,30 @@ class AsyncmyDriver(
162
255
  async def select_one_or_none(
163
256
  self,
164
257
  sql: str,
165
- parameters: Optional["StatementParameterType"] = None,
258
+ parameters: "Optional[StatementParameterType]" = None,
166
259
  /,
167
- *,
168
- connection: Optional["AsyncmyConnection"] = None,
260
+ *filters: "StatementFilter",
261
+ connection: "Optional[AsyncmyConnection]" = None,
169
262
  schema_type: "Optional[type[ModelDTOT]]" = None,
170
263
  **kwargs: Any,
171
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
264
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
172
265
  """Fetch one row from the database.
173
266
 
174
267
  Returns:
175
268
  The first row of the query results.
176
269
  """
177
270
  connection = self._connection(connection)
178
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
271
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
179
272
  async with self._with_cursor(connection) as cursor:
180
273
  await cursor.execute(sql, parameters)
181
274
  result = await cursor.fetchone()
182
275
  if result is None:
183
276
  return None
184
277
  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))))
278
+
279
+ # Convert to dict and use ResultConverter
280
+ dict_result = dict(zip(column_names, result))
281
+ return self.to_schema(dict_result, schema_type=schema_type)
188
282
 
189
283
  @overload
190
284
  async def select_value(
@@ -192,7 +286,7 @@ class AsyncmyDriver(
192
286
  sql: str,
193
287
  parameters: "Optional[StatementParameterType]" = None,
194
288
  /,
195
- *,
289
+ *filters: "StatementFilter",
196
290
  connection: "Optional[AsyncmyConnection]" = None,
197
291
  schema_type: None = None,
198
292
  **kwargs: Any,
@@ -203,7 +297,7 @@ class AsyncmyDriver(
203
297
  sql: str,
204
298
  parameters: "Optional[StatementParameterType]" = None,
205
299
  /,
206
- *,
300
+ *filters: "StatementFilter",
207
301
  connection: "Optional[AsyncmyConnection]" = None,
208
302
  schema_type: "type[T]",
209
303
  **kwargs: Any,
@@ -213,7 +307,7 @@ class AsyncmyDriver(
213
307
  sql: str,
214
308
  parameters: "Optional[StatementParameterType]" = None,
215
309
  /,
216
- *,
310
+ *filters: "StatementFilter",
217
311
  connection: "Optional[AsyncmyConnection]" = None,
218
312
  schema_type: "Optional[type[T]]" = None,
219
313
  **kwargs: Any,
@@ -221,16 +315,14 @@ class AsyncmyDriver(
221
315
  """Fetch a single value from the database.
222
316
 
223
317
  Returns:
224
- The first value from the first row of results, or None if no results.
318
+ The first value from the first row of results.
225
319
  """
226
320
  connection = self._connection(connection)
227
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
228
-
321
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
229
322
  async with self._with_cursor(connection) as cursor:
230
323
  await cursor.execute(sql, parameters)
231
324
  result = await cursor.fetchone()
232
325
  result = self.check_not_found(result)
233
-
234
326
  value = result[0]
235
327
  if schema_type is not None:
236
328
  return schema_type(value) # type: ignore[call-arg]
@@ -242,7 +334,7 @@ class AsyncmyDriver(
242
334
  sql: str,
243
335
  parameters: "Optional[StatementParameterType]" = None,
244
336
  /,
245
- *,
337
+ *filters: "StatementFilter",
246
338
  connection: "Optional[AsyncmyConnection]" = None,
247
339
  schema_type: None = None,
248
340
  **kwargs: Any,
@@ -253,7 +345,7 @@ class AsyncmyDriver(
253
345
  sql: str,
254
346
  parameters: "Optional[StatementParameterType]" = None,
255
347
  /,
256
- *,
348
+ *filters: "StatementFilter",
257
349
  connection: "Optional[AsyncmyConnection]" = None,
258
350
  schema_type: "type[T]",
259
351
  **kwargs: Any,
@@ -263,7 +355,7 @@ class AsyncmyDriver(
263
355
  sql: str,
264
356
  parameters: "Optional[StatementParameterType]" = None,
265
357
  /,
266
- *,
358
+ *filters: "StatementFilter",
267
359
  connection: "Optional[AsyncmyConnection]" = None,
268
360
  schema_type: "Optional[type[T]]" = None,
269
361
  **kwargs: Any,
@@ -274,15 +366,12 @@ class AsyncmyDriver(
274
366
  The first value from the first row of results, or None if no results.
275
367
  """
276
368
  connection = self._connection(connection)
277
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
278
-
369
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
279
370
  async with self._with_cursor(connection) as cursor:
280
371
  await cursor.execute(sql, parameters)
281
372
  result = await cursor.fetchone()
282
-
283
373
  if result is None:
284
374
  return None
285
-
286
375
  value = result[0]
287
376
  if schema_type is not None:
288
377
  return schema_type(value) # type: ignore[call-arg]
@@ -291,10 +380,10 @@ class AsyncmyDriver(
291
380
  async def insert_update_delete(
292
381
  self,
293
382
  sql: str,
294
- parameters: Optional["StatementParameterType"] = None,
383
+ parameters: "Optional[StatementParameterType]" = None,
295
384
  /,
296
- *,
297
- connection: Optional["AsyncmyConnection"] = None,
385
+ *filters: "StatementFilter",
386
+ connection: "Optional[AsyncmyConnection]" = None,
298
387
  **kwargs: Any,
299
388
  ) -> int:
300
389
  """Insert, update, or delete data from the database.
@@ -303,8 +392,7 @@ class AsyncmyDriver(
303
392
  Row count affected by the operation.
304
393
  """
305
394
  connection = self._connection(connection)
306
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
307
-
395
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
308
396
  async with self._with_cursor(connection) as cursor:
309
397
  await cursor.execute(sql, parameters)
310
398
  return cursor.rowcount
@@ -315,7 +403,7 @@ class AsyncmyDriver(
315
403
  sql: str,
316
404
  parameters: "Optional[StatementParameterType]" = None,
317
405
  /,
318
- *,
406
+ *filters: "StatementFilter",
319
407
  connection: "Optional[AsyncmyConnection]" = None,
320
408
  schema_type: None = None,
321
409
  **kwargs: Any,
@@ -326,7 +414,7 @@ class AsyncmyDriver(
326
414
  sql: str,
327
415
  parameters: "Optional[StatementParameterType]" = None,
328
416
  /,
329
- *,
417
+ *filters: "StatementFilter",
330
418
  connection: "Optional[AsyncmyConnection]" = None,
331
419
  schema_type: "type[ModelDTOT]",
332
420
  **kwargs: Any,
@@ -334,21 +422,20 @@ class AsyncmyDriver(
334
422
  async def insert_update_delete_returning(
335
423
  self,
336
424
  sql: str,
337
- parameters: Optional["StatementParameterType"] = None,
425
+ parameters: "Optional[StatementParameterType]" = None,
338
426
  /,
339
- *,
340
- connection: Optional["AsyncmyConnection"] = None,
427
+ *filters: "StatementFilter",
428
+ connection: "Optional[AsyncmyConnection]" = None,
341
429
  schema_type: "Optional[type[ModelDTOT]]" = None,
342
430
  **kwargs: Any,
343
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
431
+ ) -> "Union[dict[str, Any], ModelDTOT]":
344
432
  """Insert, update, or delete data from the database and return result.
345
433
 
346
434
  Returns:
347
435
  The first row of results.
348
436
  """
349
437
  connection = self._connection(connection)
350
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
351
- column_names: list[str] = []
438
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
352
439
 
353
440
  async with self._with_cursor(connection) as cursor:
354
441
  await cursor.execute(sql, parameters)
@@ -356,17 +443,16 @@ class AsyncmyDriver(
356
443
  if result is None:
357
444
  return None
358
445
  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))
446
+
447
+ # Convert to dict and use ResultConverter
448
+ dict_result = dict(zip(column_names, result))
449
+ return self.to_schema(dict_result, schema_type=schema_type)
362
450
 
363
451
  async def execute_script(
364
452
  self,
365
453
  sql: str,
366
- parameters: Optional["StatementParameterType"] = None,
367
- /,
368
- *,
369
- connection: Optional["AsyncmyConnection"] = None,
454
+ parameters: "Optional[StatementParameterType]" = None,
455
+ connection: "Optional[AsyncmyConnection]" = None,
370
456
  **kwargs: Any,
371
457
  ) -> str:
372
458
  """Execute a script.
@@ -376,7 +462,17 @@ class AsyncmyDriver(
376
462
  """
377
463
  connection = self._connection(connection)
378
464
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
379
-
380
465
  async with self._with_cursor(connection) as cursor:
381
466
  await cursor.execute(sql, parameters)
382
- return "DONE"
467
+ return f"Script executed successfully. Rows affected: {cursor.rowcount}"
468
+
469
+ def _connection(self, connection: "Optional[AsyncmyConnection]" = None) -> "AsyncmyConnection":
470
+ """Get the connection to use for the operation.
471
+
472
+ Args:
473
+ connection: Optional connection to use.
474
+
475
+ Returns:
476
+ The connection to use.
477
+ """
478
+ 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]