sqlspec 0.8.0__py3-none-any.whl → 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (45) hide show
  1. sqlspec/_typing.py +39 -6
  2. sqlspec/adapters/adbc/__init__.py +2 -2
  3. sqlspec/adapters/adbc/config.py +34 -11
  4. sqlspec/adapters/adbc/driver.py +302 -111
  5. sqlspec/adapters/aiosqlite/__init__.py +2 -2
  6. sqlspec/adapters/aiosqlite/config.py +2 -2
  7. sqlspec/adapters/aiosqlite/driver.py +164 -42
  8. sqlspec/adapters/asyncmy/__init__.py +3 -3
  9. sqlspec/adapters/asyncmy/config.py +11 -12
  10. sqlspec/adapters/asyncmy/driver.py +161 -37
  11. sqlspec/adapters/asyncpg/__init__.py +5 -5
  12. sqlspec/adapters/asyncpg/config.py +17 -19
  13. sqlspec/adapters/asyncpg/driver.py +386 -96
  14. sqlspec/adapters/duckdb/__init__.py +2 -2
  15. sqlspec/adapters/duckdb/config.py +2 -2
  16. sqlspec/adapters/duckdb/driver.py +190 -60
  17. sqlspec/adapters/oracledb/__init__.py +8 -8
  18. sqlspec/adapters/oracledb/config/__init__.py +6 -6
  19. sqlspec/adapters/oracledb/config/_asyncio.py +9 -10
  20. sqlspec/adapters/oracledb/config/_sync.py +8 -9
  21. sqlspec/adapters/oracledb/driver.py +384 -45
  22. sqlspec/adapters/psqlpy/__init__.py +0 -0
  23. sqlspec/adapters/psqlpy/config.py +250 -0
  24. sqlspec/adapters/psqlpy/driver.py +481 -0
  25. sqlspec/adapters/psycopg/__init__.py +10 -5
  26. sqlspec/adapters/psycopg/config/__init__.py +6 -6
  27. sqlspec/adapters/psycopg/config/_async.py +12 -12
  28. sqlspec/adapters/psycopg/config/_sync.py +13 -13
  29. sqlspec/adapters/psycopg/driver.py +432 -222
  30. sqlspec/adapters/sqlite/__init__.py +2 -2
  31. sqlspec/adapters/sqlite/config.py +2 -2
  32. sqlspec/adapters/sqlite/driver.py +176 -72
  33. sqlspec/base.py +687 -161
  34. sqlspec/exceptions.py +30 -0
  35. sqlspec/extensions/litestar/config.py +6 -0
  36. sqlspec/extensions/litestar/handlers.py +25 -0
  37. sqlspec/extensions/litestar/plugin.py +8 -1
  38. sqlspec/statement.py +373 -0
  39. sqlspec/typing.py +10 -1
  40. {sqlspec-0.8.0.dist-info → sqlspec-0.9.1.dist-info}/METADATA +144 -2
  41. sqlspec-0.9.1.dist-info/RECORD +61 -0
  42. sqlspec-0.8.0.dist-info/RECORD +0 -57
  43. {sqlspec-0.8.0.dist-info → sqlspec-0.9.1.dist-info}/WHEEL +0 -0
  44. {sqlspec-0.8.0.dist-info → sqlspec-0.9.1.dist-info}/licenses/LICENSE +0 -0
  45. {sqlspec-0.8.0.dist-info → sqlspec-0.9.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,29 +1,142 @@
1
+ import logging
1
2
  from contextlib import asynccontextmanager, contextmanager
2
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
3
4
 
4
5
  from psycopg.rows import dict_row
5
6
 
6
- from sqlspec.base import PARAM_REGEX, AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol, T
7
+ from sqlspec.base import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol, T
8
+ from sqlspec.exceptions import SQLParsingError
9
+ from sqlspec.statement import PARAM_REGEX, SQLStatement
7
10
 
8
11
  if TYPE_CHECKING:
9
- from collections.abc import AsyncGenerator, Generator
12
+ from collections.abc import AsyncGenerator, Generator, Sequence
10
13
 
11
14
  from psycopg import AsyncConnection, Connection
12
15
 
13
16
  from sqlspec.typing import ModelDTOT, StatementParameterType
14
17
 
18
+ logger = logging.getLogger("sqlspec")
19
+
15
20
  __all__ = ("PsycopgAsyncDriver", "PsycopgSyncDriver")
16
21
 
17
22
 
18
- class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
23
+ class PsycopgParameterParser:
24
+ dialect: str
25
+
26
+ def _process_sql_params(
27
+ self,
28
+ sql: str,
29
+ parameters: "Optional[StatementParameterType]" = None,
30
+ /,
31
+ **kwargs: Any,
32
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
33
+ """Process SQL and parameters, converting :name -> %(name)s if needed."""
34
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
35
+ processed_sql, processed_params = stmt.process()
36
+
37
+ if isinstance(processed_params, dict):
38
+ parameter_dict = processed_params
39
+ processed_sql_parts: list[str] = []
40
+ last_end = 0
41
+ found_params_regex: list[str] = []
42
+
43
+ for match in PARAM_REGEX.finditer(processed_sql):
44
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
45
+ continue
46
+
47
+ if match.group("var_name"):
48
+ var_name = match.group("var_name")
49
+ found_params_regex.append(var_name)
50
+ start = match.start("var_name") - 1
51
+ end = match.end("var_name")
52
+
53
+ if var_name not in parameter_dict:
54
+ msg = (
55
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
56
+ f"Processed SQL: {processed_sql}"
57
+ )
58
+ raise SQLParsingError(msg)
59
+
60
+ processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
61
+ last_end = end
62
+
63
+ processed_sql_parts.append(processed_sql[last_end:])
64
+ final_sql = "".join(processed_sql_parts)
65
+
66
+ if not found_params_regex and parameter_dict:
67
+ logger.warning(
68
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
69
+ list(parameter_dict.keys()),
70
+ processed_sql,
71
+ )
72
+ return processed_sql, parameter_dict
73
+
74
+ return final_sql, parameter_dict
75
+
76
+ return processed_sql, processed_params
77
+
78
+
79
+ class PsycopgSyncDriver(PsycopgParameterParser, SyncDriverAdapterProtocol["Connection"]):
19
80
  """Psycopg Sync Driver Adapter."""
20
81
 
21
82
  connection: "Connection"
22
- param_style: str = "%s"
83
+ dialect: str = "postgres"
23
84
 
24
85
  def __init__(self, connection: "Connection") -> None:
25
86
  self.connection = connection
26
87
 
88
+ def _process_sql_params(
89
+ self,
90
+ sql: str,
91
+ parameters: "Optional[StatementParameterType]" = None,
92
+ /,
93
+ **kwargs: Any,
94
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
95
+ """Process SQL and parameters, converting :name -> %(name)s if needed."""
96
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
97
+ processed_sql, processed_params = stmt.process()
98
+
99
+ if isinstance(processed_params, dict):
100
+ parameter_dict = processed_params
101
+ processed_sql_parts: list[str] = []
102
+ last_end = 0
103
+ found_params_regex: list[str] = []
104
+
105
+ for match in PARAM_REGEX.finditer(processed_sql):
106
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
107
+ continue
108
+
109
+ if match.group("var_name"):
110
+ var_name = match.group("var_name")
111
+ found_params_regex.append(var_name)
112
+ start = match.start("var_name") - 1
113
+ end = match.end("var_name")
114
+
115
+ if var_name not in parameter_dict:
116
+ msg = (
117
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
118
+ f"Processed SQL: {processed_sql}"
119
+ )
120
+ raise SQLParsingError(msg)
121
+
122
+ processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
123
+ last_end = end
124
+
125
+ processed_sql_parts.append(processed_sql[last_end:])
126
+ final_sql = "".join(processed_sql_parts)
127
+
128
+ if not found_params_regex and parameter_dict:
129
+ logger.warning(
130
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
131
+ list(parameter_dict.keys()),
132
+ processed_sql,
133
+ )
134
+ return processed_sql, parameter_dict
135
+
136
+ return final_sql, parameter_dict
137
+
138
+ return processed_sql, processed_params
139
+
27
140
  @staticmethod
28
141
  @contextmanager
29
142
  def _with_cursor(connection: "Connection") -> "Generator[Any, None, None]":
@@ -33,83 +146,46 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
33
146
  finally:
34
147
  cursor.close()
35
148
 
36
- def _process_sql_params(
37
- self, sql: str, parameters: "Optional[StatementParameterType]" = None
38
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
39
- """Process SQL query and parameters for DB-API execution.
40
-
41
- Converts named parameters (:name) to positional parameters (%s)
42
- if the input parameters are a dictionary.
43
-
44
- Args:
45
- sql: The SQL query string.
46
- parameters: The parameters for the query (dict, tuple, list, or None).
47
-
48
- Returns:
49
- A tuple containing the processed SQL string and the processed parameters
50
- (always a tuple or None if the input was a dictionary, otherwise the original type).
51
-
52
- Raises:
53
- ValueError: If a named parameter in the SQL is not found in the dictionary
54
- or if a parameter in the dictionary is not used in the SQL.
55
- """
56
- if not isinstance(parameters, dict) or not parameters:
57
- # If parameters are not a dict, or empty dict, assume positional/no params
58
- # Let the underlying driver handle tuples/lists directly
59
- return sql, parameters
60
-
61
- processed_sql = ""
62
- processed_params_list: list[Any] = []
63
- last_end = 0
64
- found_params: set[str] = set()
65
-
66
- for match in PARAM_REGEX.finditer(sql):
67
- if match.group("dquote") is not None or match.group("squote") is not None:
68
- # Skip placeholders within quotes
69
- continue
70
-
71
- var_name = match.group("var_name")
72
- if var_name is None: # Should not happen with the regex, but safeguard
73
- continue
74
-
75
- if var_name not in parameters:
76
- msg = f"Named parameter ':{var_name}' found in SQL but not provided in parameters dictionary."
77
- raise ValueError(msg)
78
-
79
- # Append segment before the placeholder + the driver's positional placeholder
80
- processed_sql += sql[last_end : match.start("var_name") - 1] + "%s"
81
- processed_params_list.append(parameters[var_name])
82
- found_params.add(var_name)
83
- last_end = match.end("var_name")
84
-
85
- # Append the rest of the SQL string
86
- processed_sql += sql[last_end:]
87
-
88
- # Check if all provided parameters were used
89
- unused_params = set(parameters.keys()) - found_params
90
- if unused_params:
91
- msg = f"Parameters provided but not found in SQL: {unused_params}"
92
- # Depending on desired strictness, this could be a warning or an error
93
- # For now, let's raise an error for clarity
94
- raise ValueError(msg)
95
-
96
- return processed_sql, tuple(processed_params_list)
97
-
149
+ # --- Public API Methods --- #
150
+ @overload
98
151
  def select(
99
152
  self,
100
153
  sql: str,
101
154
  parameters: "Optional[StatementParameterType]" = None,
102
155
  /,
156
+ *,
103
157
  connection: "Optional[Connection]" = None,
158
+ schema_type: None = None,
159
+ **kwargs: Any,
160
+ ) -> "Sequence[dict[str, Any]]": ...
161
+ @overload
162
+ def select(
163
+ self,
164
+ sql: str,
165
+ parameters: "Optional[StatementParameterType]" = None,
166
+ /,
167
+ *,
168
+ connection: "Optional[Connection]" = None,
169
+ schema_type: "type[ModelDTOT]",
170
+ **kwargs: Any,
171
+ ) -> "Sequence[ModelDTOT]": ...
172
+ def select(
173
+ self,
174
+ sql: str,
175
+ parameters: "Optional[StatementParameterType]" = None,
176
+ /,
177
+ *,
104
178
  schema_type: "Optional[type[ModelDTOT]]" = None,
105
- ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
179
+ connection: "Optional[Connection]" = None,
180
+ **kwargs: Any,
181
+ ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
106
182
  """Fetch data from the database.
107
183
 
108
184
  Returns:
109
185
  List of row data as either model instances or dictionaries.
110
186
  """
111
187
  connection = self._connection(connection)
112
- sql, parameters = self._process_sql_params(sql, parameters)
188
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
113
189
  with self._with_cursor(connection) as cursor:
114
190
  cursor.execute(sql, parameters)
115
191
  results = cursor.fetchall()
@@ -120,13 +196,37 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
120
196
  return [cast("ModelDTOT", schema_type(**row)) for row in results] # pyright: ignore[reportUnknownArgumentType]
121
197
  return [cast("dict[str,Any]", row) for row in results] # pyright: ignore[reportUnknownArgumentType]
122
198
 
199
+ @overload
200
+ def select_one(
201
+ self,
202
+ sql: str,
203
+ parameters: "Optional[StatementParameterType]" = None,
204
+ /,
205
+ *,
206
+ connection: "Optional[Connection]" = None,
207
+ schema_type: None = None,
208
+ **kwargs: Any,
209
+ ) -> "dict[str, Any]": ...
210
+ @overload
211
+ def select_one(
212
+ self,
213
+ sql: str,
214
+ parameters: "Optional[StatementParameterType]" = None,
215
+ /,
216
+ *,
217
+ connection: "Optional[Connection]" = None,
218
+ schema_type: "type[ModelDTOT]",
219
+ **kwargs: Any,
220
+ ) -> "ModelDTOT": ...
123
221
  def select_one(
124
222
  self,
125
223
  sql: str,
126
224
  parameters: "Optional[StatementParameterType]" = None,
127
225
  /,
226
+ *,
128
227
  connection: "Optional[Connection]" = None,
129
228
  schema_type: "Optional[type[ModelDTOT]]" = None,
229
+ **kwargs: Any,
130
230
  ) -> "Union[ModelDTOT, dict[str, Any]]":
131
231
  """Fetch one row from the database.
132
232
 
@@ -134,8 +234,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
134
234
  The first row of the query results.
135
235
  """
136
236
  connection = self._connection(connection)
137
- sql, parameters = self._process_sql_params(sql, parameters)
138
-
237
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
139
238
  with self._with_cursor(connection) as cursor:
140
239
  cursor.execute(sql, parameters)
141
240
  row = cursor.fetchone()
@@ -144,13 +243,37 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
144
243
  return cast("ModelDTOT", schema_type(**cast("dict[str,Any]", row)))
145
244
  return cast("dict[str,Any]", row)
146
245
 
246
+ @overload
247
+ def select_one_or_none(
248
+ self,
249
+ sql: str,
250
+ parameters: "Optional[StatementParameterType]" = None,
251
+ /,
252
+ *,
253
+ connection: "Optional[Connection]" = None,
254
+ schema_type: None = None,
255
+ **kwargs: Any,
256
+ ) -> "Optional[dict[str, Any]]": ...
257
+ @overload
147
258
  def select_one_or_none(
148
259
  self,
149
260
  sql: str,
150
261
  parameters: "Optional[StatementParameterType]" = None,
151
262
  /,
263
+ *,
264
+ connection: "Optional[Connection]" = None,
265
+ schema_type: "type[ModelDTOT]",
266
+ **kwargs: Any,
267
+ ) -> "Optional[ModelDTOT]": ...
268
+ def select_one_or_none(
269
+ self,
270
+ sql: str,
271
+ parameters: "Optional[StatementParameterType]" = None,
272
+ /,
273
+ *,
152
274
  connection: "Optional[Connection]" = None,
153
275
  schema_type: "Optional[type[ModelDTOT]]" = None,
276
+ **kwargs: Any,
154
277
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
155
278
  """Fetch one row from the database.
156
279
 
@@ -158,8 +281,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
158
281
  The first row of the query results.
159
282
  """
160
283
  connection = self._connection(connection)
161
- sql, parameters = self._process_sql_params(sql, parameters)
162
-
284
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
163
285
  with self._with_cursor(connection) as cursor:
164
286
  cursor.execute(sql, parameters)
165
287
  row = cursor.fetchone()
@@ -169,13 +291,37 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
169
291
  return cast("ModelDTOT", schema_type(**cast("dict[str,Any]", row)))
170
292
  return cast("dict[str,Any]", row)
171
293
 
294
+ @overload
295
+ def select_value(
296
+ self,
297
+ sql: str,
298
+ parameters: "Optional[StatementParameterType]" = None,
299
+ /,
300
+ *,
301
+ connection: "Optional[Connection]" = None,
302
+ schema_type: None = None,
303
+ **kwargs: Any,
304
+ ) -> "Any": ...
305
+ @overload
172
306
  def select_value(
173
307
  self,
174
308
  sql: str,
175
309
  parameters: "Optional[StatementParameterType]" = None,
176
310
  /,
311
+ *,
312
+ connection: "Optional[Connection]" = None,
313
+ schema_type: "type[T]",
314
+ **kwargs: Any,
315
+ ) -> "T": ...
316
+ def select_value(
317
+ self,
318
+ sql: str,
319
+ parameters: "Optional[StatementParameterType]" = None,
320
+ /,
321
+ *,
177
322
  connection: "Optional[Connection]" = None,
178
323
  schema_type: "Optional[type[T]]" = None,
324
+ **kwargs: Any,
179
325
  ) -> "Union[T, Any]":
180
326
  """Fetch a single value from the database.
181
327
 
@@ -183,24 +329,48 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
183
329
  The first value from the first row of results, or None if no results.
184
330
  """
185
331
  connection = self._connection(connection)
186
- sql, parameters = self._process_sql_params(sql, parameters)
187
-
332
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
188
333
  with self._with_cursor(connection) as cursor:
189
334
  cursor.execute(sql, parameters)
190
335
  row = cursor.fetchone()
191
336
  row = self.check_not_found(row)
192
- val = next(iter(row))
337
+ val = next(iter(row.values())) if row else None
338
+ val = self.check_not_found(val)
193
339
  if schema_type is not None:
194
340
  return schema_type(val) # type: ignore[call-arg]
195
341
  return val
196
342
 
343
+ @overload
197
344
  def select_value_or_none(
198
345
  self,
199
346
  sql: str,
200
347
  parameters: "Optional[StatementParameterType]" = None,
201
348
  /,
349
+ *,
350
+ connection: "Optional[Connection]" = None,
351
+ schema_type: None = None,
352
+ **kwargs: Any,
353
+ ) -> "Optional[Any]": ...
354
+ @overload
355
+ def select_value_or_none(
356
+ self,
357
+ sql: str,
358
+ parameters: "Optional[StatementParameterType]" = None,
359
+ /,
360
+ *,
361
+ connection: "Optional[Connection]" = None,
362
+ schema_type: "type[T]",
363
+ **kwargs: Any,
364
+ ) -> "Optional[T]": ...
365
+ def select_value_or_none(
366
+ self,
367
+ sql: str,
368
+ parameters: "Optional[StatementParameterType]" = None,
369
+ /,
370
+ *,
202
371
  connection: "Optional[Connection]" = None,
203
372
  schema_type: "Optional[type[T]]" = None,
373
+ **kwargs: Any,
204
374
  ) -> "Optional[Union[T, Any]]":
205
375
  """Fetch a single value from the database.
206
376
 
@@ -208,14 +378,15 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
208
378
  The first value from the first row of results, or None if no results.
209
379
  """
210
380
  connection = self._connection(connection)
211
- sql, parameters = self._process_sql_params(sql, parameters)
212
-
381
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
213
382
  with self._with_cursor(connection) as cursor:
214
383
  cursor.execute(sql, parameters)
215
384
  row = cursor.fetchone()
216
385
  if row is None:
217
386
  return None
218
- val = next(iter(row))
387
+ val = next(iter(row.values())) if row else None
388
+ if val is None:
389
+ return None
219
390
  if schema_type is not None:
220
391
  return schema_type(val) # type: ignore[call-arg]
221
392
  return val
@@ -225,27 +396,52 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
225
396
  sql: str,
226
397
  parameters: "Optional[StatementParameterType]" = None,
227
398
  /,
399
+ *,
228
400
  connection: "Optional[Connection]" = None,
401
+ **kwargs: Any,
229
402
  ) -> int:
230
- """Insert, update, or delete data from the database.
403
+ """Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
231
404
 
232
405
  Returns:
233
- Row count affected by the operation.
406
+ The number of rows affected by the operation.
234
407
  """
235
408
  connection = self._connection(connection)
236
- sql, parameters = self._process_sql_params(sql, parameters)
237
-
409
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
238
410
  with self._with_cursor(connection) as cursor:
239
411
  cursor.execute(sql, parameters)
240
- return cursor.rowcount if hasattr(cursor, "rowcount") else -1
412
+ return getattr(cursor, "rowcount", -1) # pyright: ignore[reportUnknownMemberType]
241
413
 
414
+ @overload
242
415
  def insert_update_delete_returning(
243
416
  self,
244
417
  sql: str,
245
418
  parameters: "Optional[StatementParameterType]" = None,
246
419
  /,
420
+ *,
421
+ connection: "Optional[Connection]" = None,
422
+ schema_type: None = None,
423
+ **kwargs: Any,
424
+ ) -> "dict[str, Any]": ...
425
+ @overload
426
+ def insert_update_delete_returning(
427
+ self,
428
+ sql: str,
429
+ parameters: "Optional[StatementParameterType]" = None,
430
+ /,
431
+ *,
432
+ connection: "Optional[Connection]" = None,
433
+ schema_type: "type[ModelDTOT]",
434
+ **kwargs: Any,
435
+ ) -> "ModelDTOT": ...
436
+ def insert_update_delete_returning(
437
+ self,
438
+ sql: str,
439
+ parameters: "Optional[StatementParameterType]" = None,
440
+ /,
441
+ *,
247
442
  connection: "Optional[Connection]" = None,
248
443
  schema_type: "Optional[type[ModelDTOT]]" = None,
444
+ **kwargs: Any,
249
445
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
250
446
  """Insert, update, or delete data from the database and return result.
251
447
 
@@ -253,8 +449,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
253
449
  The first row of results.
254
450
  """
255
451
  connection = self._connection(connection)
256
- sql, parameters = self._process_sql_params(sql, parameters)
257
-
452
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
258
453
  with self._with_cursor(connection) as cursor:
259
454
  cursor.execute(sql, parameters)
260
455
  result = cursor.fetchone()
@@ -271,7 +466,9 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
271
466
  sql: str,
272
467
  parameters: "Optional[StatementParameterType]" = None,
273
468
  /,
469
+ *,
274
470
  connection: "Optional[Connection]" = None,
471
+ **kwargs: Any,
275
472
  ) -> str:
276
473
  """Execute a script.
277
474
 
@@ -279,45 +476,17 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
279
476
  Status message for the operation.
280
477
  """
281
478
  connection = self._connection(connection)
282
- sql, parameters = self._process_sql_params(sql, parameters)
283
-
479
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
284
480
  with self._with_cursor(connection) as cursor:
285
481
  cursor.execute(sql, parameters)
286
- return str(cursor.rowcount)
287
-
288
- def execute_script_returning(
289
- self,
290
- sql: str,
291
- parameters: "Optional[StatementParameterType]" = None,
292
- /,
293
- connection: "Optional[Connection]" = None,
294
- schema_type: "Optional[type[ModelDTOT]]" = None,
295
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
296
- """Execute a script and return result.
297
-
298
- Returns:
299
- The first row of results.
300
- """
301
- connection = self._connection(connection)
302
- sql, parameters = self._process_sql_params(sql, parameters)
303
-
304
- with self._with_cursor(connection) as cursor:
305
- cursor.execute(sql, parameters)
306
- result = cursor.fetchone()
307
-
308
- if result is None:
309
- return None
310
-
311
- if schema_type is not None:
312
- return cast("ModelDTOT", schema_type(**result)) # pyright: ignore[reportUnknownArgumentType]
313
- return cast("dict[str, Any]", result) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
482
+ return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"
314
483
 
315
484
 
316
- class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
485
+ class PsycopgAsyncDriver(PsycopgParameterParser, AsyncDriverAdapterProtocol["AsyncConnection"]):
317
486
  """Psycopg Async Driver Adapter."""
318
487
 
319
488
  connection: "AsyncConnection"
320
- param_style: str = "%s"
489
+ dialect: str = "postgres"
321
490
 
322
491
  def __init__(self, connection: "AsyncConnection") -> None:
323
492
  self.connection = connection
@@ -331,83 +500,46 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
331
500
  finally:
332
501
  await cursor.close()
333
502
 
334
- def _process_sql_params(
335
- self, sql: str, parameters: "Optional[StatementParameterType]" = None
336
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
337
- """Process SQL query and parameters for DB-API execution.
338
-
339
- Converts named parameters (:name) to positional parameters (%s)
340
- if the input parameters are a dictionary.
341
-
342
- Args:
343
- sql: The SQL query string.
344
- parameters: The parameters for the query (dict, tuple, list, or None).
345
-
346
- Returns:
347
- A tuple containing the processed SQL string and the processed parameters
348
- (always a tuple or None if the input was a dictionary, otherwise the original type).
349
-
350
- Raises:
351
- ValueError: If a named parameter in the SQL is not found in the dictionary
352
- or if a parameter in the dictionary is not used in the SQL.
353
- """
354
- if not isinstance(parameters, dict) or not parameters:
355
- # If parameters are not a dict, or empty dict, assume positional/no params
356
- # Let the underlying driver handle tuples/lists directly
357
- return sql, parameters
358
-
359
- processed_sql = ""
360
- processed_params_list: list[Any] = []
361
- last_end = 0
362
- found_params: set[str] = set()
363
-
364
- for match in PARAM_REGEX.finditer(sql):
365
- if match.group("dquote") is not None or match.group("squote") is not None:
366
- # Skip placeholders within quotes
367
- continue
368
-
369
- var_name = match.group("var_name")
370
- if var_name is None: # Should not happen with the regex, but safeguard
371
- continue
372
-
373
- if var_name not in parameters:
374
- msg = f"Named parameter ':{var_name}' found in SQL but not provided in parameters dictionary."
375
- raise ValueError(msg)
376
-
377
- # Append segment before the placeholder + the driver's positional placeholder
378
- processed_sql += sql[last_end : match.start("var_name") - 1] + "%s"
379
- processed_params_list.append(parameters[var_name])
380
- found_params.add(var_name)
381
- last_end = match.end("var_name")
382
-
383
- # Append the rest of the SQL string
384
- processed_sql += sql[last_end:]
385
-
386
- # Check if all provided parameters were used
387
- unused_params = set(parameters.keys()) - found_params
388
- if unused_params:
389
- msg = f"Parameters provided but not found in SQL: {unused_params}"
390
- # Depending on desired strictness, this could be a warning or an error
391
- # For now, let's raise an error for clarity
392
- raise ValueError(msg)
393
-
394
- return processed_sql, tuple(processed_params_list)
395
-
503
+ # --- Public API Methods --- #
504
+ @overload
396
505
  async def select(
397
506
  self,
398
507
  sql: str,
399
508
  parameters: "Optional[StatementParameterType]" = None,
400
509
  /,
510
+ *,
511
+ connection: "Optional[AsyncConnection]" = None,
512
+ schema_type: None = None,
513
+ **kwargs: Any,
514
+ ) -> "Sequence[dict[str, Any]]": ...
515
+ @overload
516
+ async def select(
517
+ self,
518
+ sql: str,
519
+ parameters: "Optional[StatementParameterType]" = None,
520
+ /,
521
+ *,
522
+ connection: "Optional[AsyncConnection]" = None,
523
+ schema_type: "type[ModelDTOT]",
524
+ **kwargs: Any,
525
+ ) -> "Sequence[ModelDTOT]": ...
526
+ async def select(
527
+ self,
528
+ sql: str,
529
+ parameters: "Optional[StatementParameterType]" = None,
530
+ /,
531
+ *,
401
532
  connection: "Optional[AsyncConnection]" = None,
402
533
  schema_type: "Optional[type[ModelDTOT]]" = None,
403
- ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
534
+ **kwargs: Any,
535
+ ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
404
536
  """Fetch data from the database.
405
537
 
406
538
  Returns:
407
539
  List of row data as either model instances or dictionaries.
408
540
  """
409
541
  connection = self._connection(connection)
410
- sql, parameters = self._process_sql_params(sql, parameters)
542
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
411
543
  results: list[Union[ModelDTOT, dict[str, Any]]] = []
412
544
 
413
545
  async with self._with_cursor(connection) as cursor:
@@ -419,13 +551,37 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
419
551
  return [cast("ModelDTOT", schema_type(**cast("dict[str,Any]", row))) for row in results] # pyright: ignore[reportUnknownArgumentType]
420
552
  return [cast("dict[str,Any]", row) for row in results] # pyright: ignore[reportUnknownArgumentType]
421
553
 
554
+ @overload
422
555
  async def select_one(
423
556
  self,
424
557
  sql: str,
425
558
  parameters: "Optional[StatementParameterType]" = None,
426
559
  /,
560
+ *,
561
+ connection: "Optional[AsyncConnection]" = None,
562
+ schema_type: None = None,
563
+ **kwargs: Any,
564
+ ) -> "dict[str, Any]": ...
565
+ @overload
566
+ async def select_one(
567
+ self,
568
+ sql: str,
569
+ parameters: "Optional[StatementParameterType]" = None,
570
+ /,
571
+ *,
572
+ connection: "Optional[AsyncConnection]" = None,
573
+ schema_type: "type[ModelDTOT]",
574
+ **kwargs: Any,
575
+ ) -> "ModelDTOT": ...
576
+ async def select_one(
577
+ self,
578
+ sql: str,
579
+ parameters: "Optional[StatementParameterType]" = None,
580
+ /,
581
+ *,
427
582
  connection: "Optional[AsyncConnection]" = None,
428
583
  schema_type: "Optional[type[ModelDTOT]]" = None,
584
+ **kwargs: Any,
429
585
  ) -> "Union[ModelDTOT, dict[str, Any]]":
430
586
  """Fetch one row from the database.
431
587
 
@@ -433,7 +589,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
433
589
  The first row of the query results.
434
590
  """
435
591
  connection = self._connection(connection)
436
- sql, parameters = self._process_sql_params(sql, parameters)
592
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
437
593
 
438
594
  async with self._with_cursor(connection) as cursor:
439
595
  await cursor.execute(sql, parameters)
@@ -443,13 +599,37 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
443
599
  return cast("ModelDTOT", schema_type(**cast("dict[str,Any]", row)))
444
600
  return cast("dict[str,Any]", row)
445
601
 
602
+ @overload
446
603
  async def select_one_or_none(
447
604
  self,
448
605
  sql: str,
449
606
  parameters: "Optional[StatementParameterType]" = None,
450
607
  /,
608
+ *,
451
609
  connection: "Optional[AsyncConnection]" = None,
610
+ schema_type: None = None,
611
+ **kwargs: Any,
612
+ ) -> "Optional[dict[str, Any]]": ...
613
+ @overload
614
+ async def select_one_or_none(
615
+ self,
616
+ sql: str,
617
+ parameters: "Optional[StatementParameterType]" = None,
618
+ /,
619
+ *,
620
+ connection: "Optional[AsyncConnection]" = None,
621
+ schema_type: "type[ModelDTOT]",
622
+ **kwargs: Any,
623
+ ) -> "Optional[ModelDTOT]": ...
624
+ async def select_one_or_none(
625
+ self,
626
+ sql: str,
627
+ parameters: "Optional[StatementParameterType]" = None,
628
+ /,
629
+ *,
452
630
  schema_type: "Optional[type[ModelDTOT]]" = None,
631
+ connection: "Optional[AsyncConnection]" = None,
632
+ **kwargs: Any,
453
633
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
454
634
  """Fetch one row from the database.
455
635
 
@@ -457,7 +637,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
457
637
  The first row of the query results.
458
638
  """
459
639
  connection = self._connection(connection)
460
- sql, parameters = self._process_sql_params(sql, parameters)
640
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
461
641
 
462
642
  async with self._with_cursor(connection) as cursor:
463
643
  await cursor.execute(sql, parameters)
@@ -468,27 +648,52 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
468
648
  return cast("ModelDTOT", schema_type(**cast("dict[str,Any]", row)))
469
649
  return cast("dict[str,Any]", row)
470
650
 
651
+ @overload
471
652
  async def select_value(
472
653
  self,
473
654
  sql: str,
474
655
  parameters: "Optional[StatementParameterType]" = None,
475
656
  /,
657
+ *,
658
+ connection: "Optional[AsyncConnection]" = None,
659
+ schema_type: None = None,
660
+ **kwargs: Any,
661
+ ) -> "Any": ...
662
+ @overload
663
+ async def select_value(
664
+ self,
665
+ sql: str,
666
+ parameters: "Optional[StatementParameterType]" = None,
667
+ /,
668
+ *,
669
+ connection: "Optional[AsyncConnection]" = None,
670
+ schema_type: "type[T]",
671
+ **kwargs: Any,
672
+ ) -> "T": ...
673
+ async def select_value(
674
+ self,
675
+ sql: str,
676
+ parameters: "Optional[StatementParameterType]" = None,
677
+ /,
678
+ *,
476
679
  connection: "Optional[AsyncConnection]" = None,
477
680
  schema_type: "Optional[type[T]]" = None,
478
- ) -> "Optional[Union[T, Any]]":
681
+ **kwargs: Any,
682
+ ) -> "Union[T, Any]":
479
683
  """Fetch a single value from the database.
480
684
 
481
685
  Returns:
482
686
  The first value from the first row of results, or None if no results.
483
687
  """
484
688
  connection = self._connection(connection)
485
- sql, parameters = self._process_sql_params(sql, parameters)
689
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
486
690
 
487
691
  async with self._with_cursor(connection) as cursor:
488
692
  await cursor.execute(sql, parameters)
489
693
  row = await cursor.fetchone()
490
694
  row = self.check_not_found(row)
491
- val = next(iter(row))
695
+ val = next(iter(row.values())) if row else None
696
+ val = self.check_not_found(val)
492
697
  if schema_type is not None:
493
698
  return schema_type(val) # type: ignore[call-arg]
494
699
  return val
@@ -498,8 +703,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
498
703
  sql: str,
499
704
  parameters: "Optional[StatementParameterType]" = None,
500
705
  /,
706
+ *,
501
707
  connection: "Optional[AsyncConnection]" = None,
502
708
  schema_type: "Optional[type[T]]" = None,
709
+ **kwargs: Any,
503
710
  ) -> "Optional[Union[T, Any]]":
504
711
  """Fetch a single value from the database.
505
712
 
@@ -507,14 +714,16 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
507
714
  The first value from the first row of results, or None if no results.
508
715
  """
509
716
  connection = self._connection(connection)
510
- sql, parameters = self._process_sql_params(sql, parameters)
717
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
511
718
 
512
719
  async with self._with_cursor(connection) as cursor:
513
720
  await cursor.execute(sql, parameters)
514
721
  row = await cursor.fetchone()
515
722
  if row is None:
516
723
  return None
517
- val = next(iter(row))
724
+ val = next(iter(row.values())) if row else None
725
+ if val is None:
726
+ return None
518
727
  if schema_type is not None:
519
728
  return schema_type(val) # type: ignore[call-arg]
520
729
  return val
@@ -524,15 +733,17 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
524
733
  sql: str,
525
734
  parameters: "Optional[StatementParameterType]" = None,
526
735
  /,
736
+ *,
527
737
  connection: "Optional[AsyncConnection]" = None,
738
+ **kwargs: Any,
528
739
  ) -> int:
529
- """Insert, update, or delete data from the database.
740
+ """Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
530
741
 
531
742
  Returns:
532
- Row count affected by the operation.
743
+ The number of rows affected by the operation.
533
744
  """
534
745
  connection = self._connection(connection)
535
- sql, parameters = self._process_sql_params(sql, parameters)
746
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
536
747
 
537
748
  async with self._with_cursor(connection) as cursor:
538
749
  await cursor.execute(sql, parameters)
@@ -542,13 +753,37 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
542
753
  rowcount = -1
543
754
  return rowcount
544
755
 
756
+ @overload
757
+ async def insert_update_delete_returning(
758
+ self,
759
+ sql: str,
760
+ parameters: "Optional[StatementParameterType]" = None,
761
+ /,
762
+ *,
763
+ connection: "Optional[AsyncConnection]" = None,
764
+ schema_type: None = None,
765
+ **kwargs: Any,
766
+ ) -> "dict[str, Any]": ...
767
+ @overload
768
+ async def insert_update_delete_returning(
769
+ self,
770
+ sql: str,
771
+ parameters: "Optional[StatementParameterType]" = None,
772
+ /,
773
+ *,
774
+ connection: "Optional[AsyncConnection]" = None,
775
+ schema_type: "type[ModelDTOT]",
776
+ **kwargs: Any,
777
+ ) -> "ModelDTOT": ...
545
778
  async def insert_update_delete_returning(
546
779
  self,
547
780
  sql: str,
548
781
  parameters: "Optional[StatementParameterType]" = None,
549
782
  /,
783
+ *,
550
784
  connection: "Optional[AsyncConnection]" = None,
551
785
  schema_type: "Optional[type[ModelDTOT]]" = None,
786
+ **kwargs: Any,
552
787
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
553
788
  """Insert, update, or delete data from the database and return result.
554
789
 
@@ -556,7 +791,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
556
791
  The first row of results.
557
792
  """
558
793
  connection = self._connection(connection)
559
- sql, parameters = self._process_sql_params(sql, parameters)
794
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
560
795
 
561
796
  async with self._with_cursor(connection) as cursor:
562
797
  await cursor.execute(sql, parameters)
@@ -574,7 +809,9 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
574
809
  sql: str,
575
810
  parameters: "Optional[StatementParameterType]" = None,
576
811
  /,
812
+ *,
577
813
  connection: "Optional[AsyncConnection]" = None,
814
+ **kwargs: Any,
578
815
  ) -> str:
579
816
  """Execute a script.
580
817
 
@@ -582,35 +819,8 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
582
819
  Status message for the operation.
583
820
  """
584
821
  connection = self._connection(connection)
585
- sql, parameters = self._process_sql_params(sql, parameters)
822
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
586
823
 
587
824
  async with self._with_cursor(connection) as cursor:
588
825
  await cursor.execute(sql, parameters)
589
- return str(cursor.rowcount)
590
-
591
- async def execute_script_returning(
592
- self,
593
- sql: str,
594
- parameters: "Optional[StatementParameterType]" = None,
595
- /,
596
- connection: "Optional[AsyncConnection]" = None,
597
- schema_type: "Optional[type[ModelDTOT]]" = None,
598
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
599
- """Execute a script and return result.
600
-
601
- Returns:
602
- The first row of results.
603
- """
604
- connection = self._connection(connection)
605
- sql, parameters = self._process_sql_params(sql, parameters)
606
-
607
- async with self._with_cursor(connection) as cursor:
608
- await cursor.execute(sql, parameters)
609
- result = await cursor.fetchone()
610
-
611
- if result is None:
612
- return None
613
-
614
- if schema_type is not None:
615
- return cast("ModelDTOT", schema_type(**result)) # pyright: ignore[reportUnknownArgumentType]
616
- return cast("dict[str, Any]", result) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
826
+ return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"