sqlspec 0.8.0__py3-none-any.whl → 0.9.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 (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 +167 -108
  5. sqlspec/adapters/aiosqlite/__init__.py +2 -2
  6. sqlspec/adapters/aiosqlite/config.py +2 -2
  7. sqlspec/adapters/aiosqlite/driver.py +28 -39
  8. sqlspec/adapters/asyncmy/__init__.py +3 -3
  9. sqlspec/adapters/asyncmy/config.py +11 -12
  10. sqlspec/adapters/asyncmy/driver.py +25 -34
  11. sqlspec/adapters/asyncpg/__init__.py +5 -5
  12. sqlspec/adapters/asyncpg/config.py +17 -19
  13. sqlspec/adapters/asyncpg/driver.py +249 -93
  14. sqlspec/adapters/duckdb/__init__.py +2 -2
  15. sqlspec/adapters/duckdb/config.py +2 -2
  16. sqlspec/adapters/duckdb/driver.py +49 -49
  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 +114 -41
  22. sqlspec/adapters/psqlpy/__init__.py +0 -0
  23. sqlspec/adapters/psqlpy/config.py +258 -0
  24. sqlspec/adapters/psqlpy/driver.py +335 -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 +180 -218
  30. sqlspec/adapters/sqlite/__init__.py +2 -2
  31. sqlspec/adapters/sqlite/config.py +2 -2
  32. sqlspec/adapters/sqlite/driver.py +43 -41
  33. sqlspec/base.py +275 -153
  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 +6 -1
  38. sqlspec/statement.py +373 -0
  39. sqlspec/typing.py +10 -1
  40. {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/METADATA +4 -1
  41. sqlspec-0.9.0.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.0.dist-info}/WHEEL +0 -0
  44. {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/licenses/LICENSE +0 -0
  45. {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,9 +1,12 @@
1
+ import logging
1
2
  from contextlib import asynccontextmanager, contextmanager
2
3
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
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
12
  from collections.abc import AsyncGenerator, Generator
@@ -12,6 +15,8 @@ if TYPE_CHECKING:
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
 
@@ -19,11 +24,63 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
19
24
  """Psycopg Sync Driver Adapter."""
20
25
 
21
26
  connection: "Connection"
22
- param_style: str = "%s"
27
+ dialect: str = "postgres"
23
28
 
24
29
  def __init__(self, connection: "Connection") -> None:
25
30
  self.connection = connection
26
31
 
32
+ def _process_sql_params(
33
+ self,
34
+ sql: str,
35
+ parameters: "Optional[StatementParameterType]" = None,
36
+ /,
37
+ **kwargs: Any,
38
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
39
+ """Process SQL and parameters, converting :name -> %(name)s if needed."""
40
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
41
+ processed_sql, processed_params = stmt.process()
42
+
43
+ if isinstance(processed_params, dict):
44
+ parameter_dict = processed_params
45
+ processed_sql_parts: list[str] = []
46
+ last_end = 0
47
+ found_params_regex: list[str] = []
48
+
49
+ for match in PARAM_REGEX.finditer(processed_sql):
50
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
51
+ continue
52
+
53
+ if match.group("var_name"):
54
+ var_name = match.group("var_name")
55
+ found_params_regex.append(var_name)
56
+ start = match.start("var_name") - 1
57
+ end = match.end("var_name")
58
+
59
+ if var_name not in parameter_dict:
60
+ msg = (
61
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
62
+ f"Processed SQL: {processed_sql}"
63
+ )
64
+ raise SQLParsingError(msg)
65
+
66
+ processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
67
+ last_end = end
68
+
69
+ processed_sql_parts.append(processed_sql[last_end:])
70
+ final_sql = "".join(processed_sql_parts)
71
+
72
+ if not found_params_regex and parameter_dict:
73
+ logger.warning(
74
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
75
+ list(parameter_dict.keys()),
76
+ processed_sql,
77
+ )
78
+ return processed_sql, parameter_dict
79
+
80
+ return final_sql, parameter_dict
81
+
82
+ return processed_sql, processed_params
83
+
27
84
  @staticmethod
28
85
  @contextmanager
29
86
  def _with_cursor(connection: "Connection") -> "Generator[Any, None, None]":
@@ -33,75 +90,15 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
33
90
  finally:
34
91
  cursor.close()
35
92
 
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
-
98
93
  def select(
99
94
  self,
100
95
  sql: str,
101
96
  parameters: "Optional[StatementParameterType]" = None,
102
97
  /,
103
- connection: "Optional[Connection]" = None,
98
+ *,
104
99
  schema_type: "Optional[type[ModelDTOT]]" = None,
100
+ connection: "Optional[Connection]" = None,
101
+ **kwargs: Any,
105
102
  ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
106
103
  """Fetch data from the database.
107
104
 
@@ -109,7 +106,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
109
106
  List of row data as either model instances or dictionaries.
110
107
  """
111
108
  connection = self._connection(connection)
112
- sql, parameters = self._process_sql_params(sql, parameters)
109
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
113
110
  with self._with_cursor(connection) as cursor:
114
111
  cursor.execute(sql, parameters)
115
112
  results = cursor.fetchall()
@@ -125,8 +122,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
125
122
  sql: str,
126
123
  parameters: "Optional[StatementParameterType]" = None,
127
124
  /,
125
+ *,
128
126
  connection: "Optional[Connection]" = None,
129
127
  schema_type: "Optional[type[ModelDTOT]]" = None,
128
+ **kwargs: Any,
130
129
  ) -> "Union[ModelDTOT, dict[str, Any]]":
131
130
  """Fetch one row from the database.
132
131
 
@@ -134,8 +133,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
134
133
  The first row of the query results.
135
134
  """
136
135
  connection = self._connection(connection)
137
- sql, parameters = self._process_sql_params(sql, parameters)
138
-
136
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
139
137
  with self._with_cursor(connection) as cursor:
140
138
  cursor.execute(sql, parameters)
141
139
  row = cursor.fetchone()
@@ -149,8 +147,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
149
147
  sql: str,
150
148
  parameters: "Optional[StatementParameterType]" = None,
151
149
  /,
150
+ *,
152
151
  connection: "Optional[Connection]" = None,
153
152
  schema_type: "Optional[type[ModelDTOT]]" = None,
153
+ **kwargs: Any,
154
154
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
155
155
  """Fetch one row from the database.
156
156
 
@@ -158,8 +158,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
158
158
  The first row of the query results.
159
159
  """
160
160
  connection = self._connection(connection)
161
- sql, parameters = self._process_sql_params(sql, parameters)
162
-
161
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
163
162
  with self._with_cursor(connection) as cursor:
164
163
  cursor.execute(sql, parameters)
165
164
  row = cursor.fetchone()
@@ -174,8 +173,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
174
173
  sql: str,
175
174
  parameters: "Optional[StatementParameterType]" = None,
176
175
  /,
176
+ *,
177
177
  connection: "Optional[Connection]" = None,
178
178
  schema_type: "Optional[type[T]]" = None,
179
+ **kwargs: Any,
179
180
  ) -> "Union[T, Any]":
180
181
  """Fetch a single value from the database.
181
182
 
@@ -183,13 +184,13 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
183
184
  The first value from the first row of results, or None if no results.
184
185
  """
185
186
  connection = self._connection(connection)
186
- sql, parameters = self._process_sql_params(sql, parameters)
187
-
187
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
188
188
  with self._with_cursor(connection) as cursor:
189
189
  cursor.execute(sql, parameters)
190
190
  row = cursor.fetchone()
191
191
  row = self.check_not_found(row)
192
- val = next(iter(row))
192
+ val = next(iter(row.values())) if row else None
193
+ val = self.check_not_found(val)
193
194
  if schema_type is not None:
194
195
  return schema_type(val) # type: ignore[call-arg]
195
196
  return val
@@ -199,8 +200,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
199
200
  sql: str,
200
201
  parameters: "Optional[StatementParameterType]" = None,
201
202
  /,
203
+ *,
202
204
  connection: "Optional[Connection]" = None,
203
205
  schema_type: "Optional[type[T]]" = None,
206
+ **kwargs: Any,
204
207
  ) -> "Optional[Union[T, Any]]":
205
208
  """Fetch a single value from the database.
206
209
 
@@ -208,14 +211,15 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
208
211
  The first value from the first row of results, or None if no results.
209
212
  """
210
213
  connection = self._connection(connection)
211
- sql, parameters = self._process_sql_params(sql, parameters)
212
-
214
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
213
215
  with self._with_cursor(connection) as cursor:
214
216
  cursor.execute(sql, parameters)
215
217
  row = cursor.fetchone()
216
218
  if row is None:
217
219
  return None
218
- val = next(iter(row))
220
+ val = next(iter(row.values())) if row else None
221
+ if val is None:
222
+ return None
219
223
  if schema_type is not None:
220
224
  return schema_type(val) # type: ignore[call-arg]
221
225
  return val
@@ -225,27 +229,30 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
225
229
  sql: str,
226
230
  parameters: "Optional[StatementParameterType]" = None,
227
231
  /,
232
+ *,
228
233
  connection: "Optional[Connection]" = None,
234
+ **kwargs: Any,
229
235
  ) -> int:
230
- """Insert, update, or delete data from the database.
236
+ """Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
231
237
 
232
238
  Returns:
233
- Row count affected by the operation.
239
+ The number of rows affected by the operation.
234
240
  """
235
241
  connection = self._connection(connection)
236
- sql, parameters = self._process_sql_params(sql, parameters)
237
-
242
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
238
243
  with self._with_cursor(connection) as cursor:
239
244
  cursor.execute(sql, parameters)
240
- return cursor.rowcount if hasattr(cursor, "rowcount") else -1
245
+ return getattr(cursor, "rowcount", -1) # pyright: ignore[reportUnknownMemberType]
241
246
 
242
247
  def insert_update_delete_returning(
243
248
  self,
244
249
  sql: str,
245
250
  parameters: "Optional[StatementParameterType]" = None,
246
251
  /,
252
+ *,
247
253
  connection: "Optional[Connection]" = None,
248
254
  schema_type: "Optional[type[ModelDTOT]]" = None,
255
+ **kwargs: Any,
249
256
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
250
257
  """Insert, update, or delete data from the database and return result.
251
258
 
@@ -253,8 +260,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
253
260
  The first row of results.
254
261
  """
255
262
  connection = self._connection(connection)
256
- sql, parameters = self._process_sql_params(sql, parameters)
257
-
263
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
258
264
  with self._with_cursor(connection) as cursor:
259
265
  cursor.execute(sql, parameters)
260
266
  result = cursor.fetchone()
@@ -271,7 +277,9 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
271
277
  sql: str,
272
278
  parameters: "Optional[StatementParameterType]" = None,
273
279
  /,
280
+ *,
274
281
  connection: "Optional[Connection]" = None,
282
+ **kwargs: Any,
275
283
  ) -> str:
276
284
  """Execute a script.
277
285
 
@@ -279,49 +287,73 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
279
287
  Status message for the operation.
280
288
  """
281
289
  connection = self._connection(connection)
282
- sql, parameters = self._process_sql_params(sql, parameters)
283
-
284
- with self._with_cursor(connection) as cursor:
285
- 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
-
290
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
304
291
  with self._with_cursor(connection) as cursor:
305
292
  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]
293
+ return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"
314
294
 
315
295
 
316
296
  class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
317
297
  """Psycopg Async Driver Adapter."""
318
298
 
319
299
  connection: "AsyncConnection"
320
- param_style: str = "%s"
300
+ dialect: str = "postgres"
321
301
 
322
302
  def __init__(self, connection: "AsyncConnection") -> None:
323
303
  self.connection = connection
324
304
 
305
+ def _process_sql_params(
306
+ self,
307
+ sql: str,
308
+ parameters: "Optional[StatementParameterType]" = None,
309
+ /,
310
+ **kwargs: Any,
311
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
312
+ """Process SQL and parameters, converting :name -> %(name)s if needed."""
313
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
314
+ processed_sql, processed_params = stmt.process()
315
+
316
+ if isinstance(processed_params, dict):
317
+ parameter_dict = processed_params
318
+ processed_sql_parts: list[str] = []
319
+ last_end = 0
320
+ found_params_regex: list[str] = []
321
+
322
+ for match in PARAM_REGEX.finditer(processed_sql):
323
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
324
+ continue
325
+
326
+ if match.group("var_name"):
327
+ var_name = match.group("var_name")
328
+ found_params_regex.append(var_name)
329
+ start = match.start("var_name") - 1
330
+ end = match.end("var_name")
331
+
332
+ if var_name not in parameter_dict:
333
+ msg = (
334
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
335
+ f"Processed SQL: {processed_sql}"
336
+ )
337
+ raise SQLParsingError(msg)
338
+
339
+ processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
340
+ last_end = end
341
+
342
+ processed_sql_parts.append(processed_sql[last_end:])
343
+ final_sql = "".join(processed_sql_parts)
344
+
345
+ if not found_params_regex and parameter_dict:
346
+ logger.warning(
347
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
348
+ list(parameter_dict.keys()),
349
+ processed_sql,
350
+ )
351
+ return processed_sql, parameter_dict
352
+
353
+ return final_sql, parameter_dict
354
+
355
+ return processed_sql, processed_params
356
+
325
357
  @staticmethod
326
358
  @asynccontextmanager
327
359
  async def _with_cursor(connection: "AsyncConnection") -> "AsyncGenerator[Any, None]":
@@ -331,75 +363,15 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
331
363
  finally:
332
364
  await cursor.close()
333
365
 
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
-
396
366
  async def select(
397
367
  self,
398
368
  sql: str,
399
369
  parameters: "Optional[StatementParameterType]" = None,
400
370
  /,
371
+ *,
401
372
  connection: "Optional[AsyncConnection]" = None,
402
373
  schema_type: "Optional[type[ModelDTOT]]" = None,
374
+ **kwargs: Any,
403
375
  ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
404
376
  """Fetch data from the database.
405
377
 
@@ -407,7 +379,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
407
379
  List of row data as either model instances or dictionaries.
408
380
  """
409
381
  connection = self._connection(connection)
410
- sql, parameters = self._process_sql_params(sql, parameters)
382
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
411
383
  results: list[Union[ModelDTOT, dict[str, Any]]] = []
412
384
 
413
385
  async with self._with_cursor(connection) as cursor:
@@ -424,8 +396,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
424
396
  sql: str,
425
397
  parameters: "Optional[StatementParameterType]" = None,
426
398
  /,
399
+ *,
427
400
  connection: "Optional[AsyncConnection]" = None,
428
401
  schema_type: "Optional[type[ModelDTOT]]" = None,
402
+ **kwargs: Any,
429
403
  ) -> "Union[ModelDTOT, dict[str, Any]]":
430
404
  """Fetch one row from the database.
431
405
 
@@ -433,7 +407,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
433
407
  The first row of the query results.
434
408
  """
435
409
  connection = self._connection(connection)
436
- sql, parameters = self._process_sql_params(sql, parameters)
410
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
437
411
 
438
412
  async with self._with_cursor(connection) as cursor:
439
413
  await cursor.execute(sql, parameters)
@@ -448,8 +422,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
448
422
  sql: str,
449
423
  parameters: "Optional[StatementParameterType]" = None,
450
424
  /,
451
- connection: "Optional[AsyncConnection]" = None,
425
+ *,
452
426
  schema_type: "Optional[type[ModelDTOT]]" = None,
427
+ connection: "Optional[AsyncConnection]" = None,
428
+ **kwargs: Any,
453
429
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
454
430
  """Fetch one row from the database.
455
431
 
@@ -457,7 +433,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
457
433
  The first row of the query results.
458
434
  """
459
435
  connection = self._connection(connection)
460
- sql, parameters = self._process_sql_params(sql, parameters)
436
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
461
437
 
462
438
  async with self._with_cursor(connection) as cursor:
463
439
  await cursor.execute(sql, parameters)
@@ -473,22 +449,25 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
473
449
  sql: str,
474
450
  parameters: "Optional[StatementParameterType]" = None,
475
451
  /,
452
+ *,
476
453
  connection: "Optional[AsyncConnection]" = None,
477
454
  schema_type: "Optional[type[T]]" = None,
478
- ) -> "Optional[Union[T, Any]]":
455
+ **kwargs: Any,
456
+ ) -> "Union[T, Any]":
479
457
  """Fetch a single value from the database.
480
458
 
481
459
  Returns:
482
460
  The first value from the first row of results, or None if no results.
483
461
  """
484
462
  connection = self._connection(connection)
485
- sql, parameters = self._process_sql_params(sql, parameters)
463
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
486
464
 
487
465
  async with self._with_cursor(connection) as cursor:
488
466
  await cursor.execute(sql, parameters)
489
467
  row = await cursor.fetchone()
490
468
  row = self.check_not_found(row)
491
- val = next(iter(row))
469
+ val = next(iter(row.values())) if row else None
470
+ val = self.check_not_found(val)
492
471
  if schema_type is not None:
493
472
  return schema_type(val) # type: ignore[call-arg]
494
473
  return val
@@ -498,8 +477,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
498
477
  sql: str,
499
478
  parameters: "Optional[StatementParameterType]" = None,
500
479
  /,
480
+ *,
501
481
  connection: "Optional[AsyncConnection]" = None,
502
482
  schema_type: "Optional[type[T]]" = None,
483
+ **kwargs: Any,
503
484
  ) -> "Optional[Union[T, Any]]":
504
485
  """Fetch a single value from the database.
505
486
 
@@ -507,14 +488,16 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
507
488
  The first value from the first row of results, or None if no results.
508
489
  """
509
490
  connection = self._connection(connection)
510
- sql, parameters = self._process_sql_params(sql, parameters)
491
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
511
492
 
512
493
  async with self._with_cursor(connection) as cursor:
513
494
  await cursor.execute(sql, parameters)
514
495
  row = await cursor.fetchone()
515
496
  if row is None:
516
497
  return None
517
- val = next(iter(row))
498
+ val = next(iter(row.values())) if row else None
499
+ if val is None:
500
+ return None
518
501
  if schema_type is not None:
519
502
  return schema_type(val) # type: ignore[call-arg]
520
503
  return val
@@ -524,15 +507,17 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
524
507
  sql: str,
525
508
  parameters: "Optional[StatementParameterType]" = None,
526
509
  /,
510
+ *,
527
511
  connection: "Optional[AsyncConnection]" = None,
512
+ **kwargs: Any,
528
513
  ) -> int:
529
- """Insert, update, or delete data from the database.
514
+ """Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
530
515
 
531
516
  Returns:
532
- Row count affected by the operation.
517
+ The number of rows affected by the operation.
533
518
  """
534
519
  connection = self._connection(connection)
535
- sql, parameters = self._process_sql_params(sql, parameters)
520
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
536
521
 
537
522
  async with self._with_cursor(connection) as cursor:
538
523
  await cursor.execute(sql, parameters)
@@ -547,8 +532,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
547
532
  sql: str,
548
533
  parameters: "Optional[StatementParameterType]" = None,
549
534
  /,
535
+ *,
550
536
  connection: "Optional[AsyncConnection]" = None,
551
537
  schema_type: "Optional[type[ModelDTOT]]" = None,
538
+ **kwargs: Any,
552
539
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
553
540
  """Insert, update, or delete data from the database and return result.
554
541
 
@@ -556,7 +543,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
556
543
  The first row of results.
557
544
  """
558
545
  connection = self._connection(connection)
559
- sql, parameters = self._process_sql_params(sql, parameters)
546
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
560
547
 
561
548
  async with self._with_cursor(connection) as cursor:
562
549
  await cursor.execute(sql, parameters)
@@ -574,7 +561,9 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
574
561
  sql: str,
575
562
  parameters: "Optional[StatementParameterType]" = None,
576
563
  /,
564
+ *,
577
565
  connection: "Optional[AsyncConnection]" = None,
566
+ **kwargs: Any,
578
567
  ) -> str:
579
568
  """Execute a script.
580
569
 
@@ -582,35 +571,8 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
582
571
  Status message for the operation.
583
572
  """
584
573
  connection = self._connection(connection)
585
- sql, parameters = self._process_sql_params(sql, parameters)
574
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
586
575
 
587
576
  async with self._with_cursor(connection) as cursor:
588
577
  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]
578
+ return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"