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,44 +1,235 @@
1
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
1
+ import logging
2
+ import re
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
2
4
 
3
5
  from asyncpg import Connection
4
6
  from typing_extensions import TypeAlias
5
7
 
6
8
  from sqlspec.base import AsyncDriverAdapterProtocol, T
9
+ from sqlspec.exceptions import SQLParsingError
10
+ from sqlspec.statement import PARAM_REGEX, SQLStatement
7
11
 
8
12
  if TYPE_CHECKING:
13
+ from collections.abc import Sequence
14
+
9
15
  from asyncpg.connection import Connection
10
16
  from asyncpg.pool import PoolConnectionProxy
11
17
 
12
18
  from sqlspec.typing import ModelDTOT, StatementParameterType
13
19
 
14
- __all__ = ("AsyncpgDriver",)
20
+ __all__ = ("AsyncpgConnection", "AsyncpgDriver")
21
+
22
+ logger = logging.getLogger("sqlspec")
15
23
 
24
+ # Regex to find '?' placeholders, skipping those inside quotes or SQL comments
25
+ # Simplified version, assumes standard SQL quoting/comments
26
+ QMARK_REGEX = re.compile(
27
+ r"""(?P<dquote>"[^"]*") | # Double-quoted strings
28
+ (?P<squote>\'[^\']*\') | # Single-quoted strings
29
+ (?P<comment>--[^\n]*|/\*.*?\*/) | # SQL comments (single/multi-line)
30
+ (?P<qmark>\?) # The question mark placeholder
31
+ """,
32
+ re.VERBOSE | re.DOTALL,
33
+ )
16
34
 
17
- PgConnection: TypeAlias = "Union[Connection[Any], PoolConnectionProxy[Any]]" # pyright: ignore[reportMissingTypeArgument]
35
+ AsyncpgConnection: TypeAlias = "Union[Connection[Any], PoolConnectionProxy[Any]]" # pyright: ignore[reportMissingTypeArgument]
18
36
 
19
37
 
20
- class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
38
+ class AsyncpgDriver(AsyncDriverAdapterProtocol["AsyncpgConnection"]):
21
39
  """AsyncPG Postgres Driver Adapter."""
22
40
 
23
- connection: "PgConnection"
41
+ connection: "AsyncpgConnection"
42
+ dialect: str = "postgres"
24
43
 
25
- def __init__(self, connection: "PgConnection") -> None:
44
+ def __init__(self, connection: "AsyncpgConnection") -> None:
26
45
  self.connection = connection
27
46
 
28
- def _process_sql_params(
29
- self, sql: str, parameters: "Optional[StatementParameterType]" = None
30
- ) -> "tuple[str, Union[tuple[Any, ...], list[Any], dict[str, Any]]]":
31
- sql, parameters = super()._process_sql_params(sql, parameters)
32
- return sql, parameters if parameters is not None else ()
33
-
47
+ def _process_sql_params( # noqa: C901, PLR0912, PLR0915
48
+ self,
49
+ sql: str,
50
+ parameters: "Optional[StatementParameterType]" = None,
51
+ /,
52
+ **kwargs: Any,
53
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
54
+ # Use SQLStatement for parameter validation and merging first
55
+ # It also handles potential dialect-specific logic if implemented there.
56
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
57
+ sql, parameters = stmt.process()
58
+
59
+ # Case 1: Parameters are effectively a dictionary (either passed as dict or via kwargs merged by SQLStatement)
60
+ if isinstance(parameters, dict):
61
+ processed_sql_parts: list[str] = []
62
+ ordered_params = []
63
+ last_end = 0
64
+ param_index = 1
65
+ found_params_regex: list[str] = []
66
+
67
+ # Manually parse the PROCESSED SQL for :name -> $n conversion
68
+ for match in PARAM_REGEX.finditer(sql):
69
+ # Skip matches inside quotes or comments
70
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
71
+ continue
72
+
73
+ if match.group("var_name"): # Finds :var_name
74
+ var_name = match.group("var_name")
75
+ found_params_regex.append(var_name)
76
+ start = match.start("var_name") - 1 # Include the ':'
77
+ end = match.end("var_name")
78
+
79
+ # SQLStatement should have already validated parameter existence,
80
+ # but we double-check here during ordering.
81
+ if var_name not in parameters:
82
+ # This should ideally not happen if SQLStatement validation is robust.
83
+ msg = (
84
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
85
+ f"Processed SQL: {sql}"
86
+ )
87
+ raise SQLParsingError(msg)
88
+
89
+ processed_sql_parts.extend((sql[last_end:start], f"${param_index}"))
90
+ ordered_params.append(parameters[var_name])
91
+ last_end = end
92
+ param_index += 1
93
+
94
+ processed_sql_parts.append(sql[last_end:])
95
+ final_sql = "".join(processed_sql_parts)
96
+
97
+ # --- Validation ---
98
+ # Check if named placeholders were found if dict params were provided
99
+ # SQLStatement might handle this validation, but a warning here can be useful.
100
+ if not found_params_regex and parameters:
101
+ logger.warning(
102
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
103
+ list(parameters.keys()),
104
+ sql,
105
+ )
106
+ # If no placeholders, return original SQL from SQLStatement and empty tuple for asyncpg
107
+ return sql, ()
108
+
109
+ # Additional checks (potentially redundant if SQLStatement covers them):
110
+ # 1. Ensure all found placeholders have corresponding params (covered by check inside loop)
111
+ # 2. Ensure all provided params correspond to a placeholder
112
+ provided_keys = set(parameters.keys())
113
+ found_keys = set(found_params_regex)
114
+ unused_keys = provided_keys - found_keys
115
+ if unused_keys:
116
+ # SQLStatement might handle this, but log a warning just in case.
117
+ logger.warning(
118
+ "Parameters provided but not used in SQL: %s. SQL: %s",
119
+ unused_keys,
120
+ sql,
121
+ )
122
+
123
+ return final_sql, tuple(ordered_params) # asyncpg expects a sequence
124
+
125
+ # Case 2: Parameters are effectively a sequence/scalar (merged by SQLStatement)
126
+ if isinstance(parameters, (list, tuple)):
127
+ # Parameters are a sequence, need to convert ? -> $n
128
+ sequence_processed_parts: list[str] = []
129
+ param_index = 1
130
+ last_end = 0
131
+ qmark_found = False
132
+
133
+ # Manually parse the PROCESSED SQL to find '?' outside comments/quotes and convert to $n
134
+ for match in QMARK_REGEX.finditer(sql):
135
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
136
+ continue # Skip quotes and comments
137
+
138
+ if match.group("qmark"):
139
+ qmark_found = True
140
+ start = match.start("qmark")
141
+ end = match.end("qmark")
142
+ sequence_processed_parts.extend((sql[last_end:start], f"${param_index}"))
143
+ last_end = end
144
+ param_index += 1
145
+
146
+ sequence_processed_parts.append(sql[last_end:])
147
+ final_sql = "".join(sequence_processed_parts)
148
+
149
+ # --- Validation ---
150
+ # Check if '?' was found if parameters were provided
151
+ if parameters and not qmark_found:
152
+ # SQLStatement might allow this, log a warning.
153
+ logger.warning(
154
+ "Sequence/scalar parameters provided, but no '?' placeholders found. SQL: %s",
155
+ sql,
156
+ )
157
+ # Return PROCESSED SQL from SQLStatement as no conversion happened here
158
+ return sql, parameters
159
+
160
+ # Check parameter count match (using count from manual parsing vs count from stmt)
161
+ expected_params = param_index - 1
162
+ actual_params = len(parameters)
163
+ if expected_params != actual_params:
164
+ msg = (
165
+ f"Parameter count mismatch: Processed SQL expected {expected_params} parameters ('$n'), "
166
+ f"but {actual_params} were provided by SQLStatement. "
167
+ f"Final Processed SQL: {final_sql}"
168
+ )
169
+ raise SQLParsingError(msg)
170
+
171
+ return final_sql, parameters
172
+
173
+ # Case 3: Parameters are None (as determined by SQLStatement)
174
+ # processed_params is None
175
+ # Check if the SQL contains any placeholders unexpectedly
176
+ # Check for :name style
177
+ named_placeholders_found = False
178
+ for match in PARAM_REGEX.finditer(sql):
179
+ if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group(
180
+ "var_name"
181
+ ):
182
+ named_placeholders_found = True
183
+ break
184
+ if named_placeholders_found:
185
+ msg = f"Processed SQL contains named parameters (:name) but no parameters were provided. SQL: {sql}"
186
+ raise SQLParsingError(msg)
187
+
188
+ # Check for ? style
189
+ qmark_placeholders_found = False
190
+ for match in QMARK_REGEX.finditer(sql):
191
+ if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group("qmark"):
192
+ qmark_placeholders_found = True
193
+ break
194
+ if qmark_placeholders_found:
195
+ msg = f"Processed SQL contains positional parameters (?) but no parameters were provided. SQL: {sql}"
196
+ raise SQLParsingError(msg)
197
+
198
+ # No parameters provided and none found in SQL, return original SQL from SQLStatement and empty tuple
199
+ return sql, () # asyncpg expects a sequence, even if empty
200
+
201
+ @overload
202
+ async def select(
203
+ self,
204
+ sql: str,
205
+ parameters: "Optional[StatementParameterType]" = None,
206
+ /,
207
+ *,
208
+ connection: "Optional[AsyncpgConnection]" = None,
209
+ schema_type: None = None,
210
+ **kwargs: Any,
211
+ ) -> "Sequence[dict[str, Any]]": ...
212
+ @overload
213
+ async def select(
214
+ self,
215
+ sql: str,
216
+ parameters: "Optional[StatementParameterType]" = None,
217
+ /,
218
+ *,
219
+ connection: "Optional[AsyncpgConnection]" = None,
220
+ schema_type: "type[ModelDTOT]",
221
+ **kwargs: Any,
222
+ ) -> "Sequence[ModelDTOT]": ...
34
223
  async def select(
35
224
  self,
36
225
  sql: str,
37
226
  parameters: Optional["StatementParameterType"] = None,
38
227
  /,
39
- connection: Optional["PgConnection"] = None,
228
+ *,
229
+ connection: Optional["AsyncpgConnection"] = None,
40
230
  schema_type: "Optional[type[ModelDTOT]]" = None,
41
- ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
231
+ **kwargs: Any,
232
+ ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
42
233
  """Fetch data from the database.
43
234
 
44
235
  Args:
@@ -46,27 +237,53 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
46
237
  parameters: Query parameters.
47
238
  connection: Optional connection to use.
48
239
  schema_type: Optional schema class for the result.
240
+ **kwargs: Additional keyword arguments.
49
241
 
50
242
  Returns:
51
243
  List of row data as either model instances or dictionaries.
52
244
  """
53
245
  connection = self._connection(connection)
54
- sql, parameters = self._process_sql_params(sql, parameters)
246
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
247
+ parameters = parameters if parameters is not None else {}
55
248
 
56
- results = await connection.fetch(sql, *parameters) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
249
+ results = await connection.fetch(sql, *parameters) # pyright: ignore
57
250
  if not results:
58
251
  return []
59
252
  if schema_type is None:
60
253
  return [dict(row.items()) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
61
254
  return [cast("ModelDTOT", schema_type(**dict(row.items()))) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
62
255
 
256
+ @overload
257
+ async def select_one(
258
+ self,
259
+ sql: str,
260
+ parameters: "Optional[StatementParameterType]" = None,
261
+ /,
262
+ *,
263
+ connection: "Optional[AsyncpgConnection]" = None,
264
+ schema_type: None = None,
265
+ **kwargs: Any,
266
+ ) -> "dict[str, Any]": ...
267
+ @overload
268
+ async def select_one(
269
+ self,
270
+ sql: str,
271
+ parameters: "Optional[StatementParameterType]" = None,
272
+ /,
273
+ *,
274
+ connection: "Optional[AsyncpgConnection]" = None,
275
+ schema_type: "type[ModelDTOT]",
276
+ **kwargs: Any,
277
+ ) -> "ModelDTOT": ...
63
278
  async def select_one(
64
279
  self,
65
280
  sql: str,
66
281
  parameters: Optional["StatementParameterType"] = None,
67
282
  /,
68
- connection: Optional["PgConnection"] = None,
283
+ *,
284
+ connection: Optional["AsyncpgConnection"] = None,
69
285
  schema_type: "Optional[type[ModelDTOT]]" = None,
286
+ **kwargs: Any,
70
287
  ) -> "Union[ModelDTOT, dict[str, Any]]":
71
288
  """Fetch one row from the database.
72
289
 
@@ -75,16 +292,15 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
75
292
  parameters: Query parameters.
76
293
  connection: Optional connection to use.
77
294
  schema_type: Optional schema class for the result.
295
+ **kwargs: Additional keyword arguments.
78
296
 
79
297
  Returns:
80
298
  The first row of the query results.
81
299
  """
82
300
  connection = self._connection(connection)
83
- sql, params = self._process_sql_params(sql, parameters)
84
- # Use empty tuple if params is None
85
- params = params if params is not None else ()
86
-
87
- result = await connection.fetchrow(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
301
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
302
+ parameters = parameters if parameters is not None else {}
303
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
88
304
  result = self.check_not_found(result)
89
305
 
90
306
  if schema_type is None:
@@ -92,13 +308,37 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
92
308
  return dict(result.items()) # type: ignore[attr-defined]
93
309
  return cast("ModelDTOT", schema_type(**dict(result.items()))) # type: ignore[attr-defined]
94
310
 
311
+ @overload
312
+ async def select_one_or_none(
313
+ self,
314
+ sql: str,
315
+ parameters: "Optional[StatementParameterType]" = None,
316
+ /,
317
+ *,
318
+ connection: "Optional[AsyncpgConnection]" = None,
319
+ schema_type: None = None,
320
+ **kwargs: Any,
321
+ ) -> "Optional[dict[str, Any]]": ...
322
+ @overload
323
+ async def select_one_or_none(
324
+ self,
325
+ sql: str,
326
+ parameters: "Optional[StatementParameterType]" = None,
327
+ /,
328
+ *,
329
+ connection: "Optional[AsyncpgConnection]" = None,
330
+ schema_type: "type[ModelDTOT]",
331
+ **kwargs: Any,
332
+ ) -> "Optional[ModelDTOT]": ...
95
333
  async def select_one_or_none(
96
334
  self,
97
335
  sql: str,
98
336
  parameters: Optional["StatementParameterType"] = None,
99
337
  /,
100
- connection: Optional["PgConnection"] = None,
338
+ *,
339
+ connection: Optional["AsyncpgConnection"] = None,
101
340
  schema_type: "Optional[type[ModelDTOT]]" = None,
341
+ **kwargs: Any,
102
342
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
103
343
  """Fetch one row from the database.
104
344
 
@@ -107,51 +347,106 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
107
347
  parameters: Query parameters.
108
348
  connection: Optional connection to use.
109
349
  schema_type: Optional schema class for the result.
350
+ **kwargs: Additional keyword arguments.
110
351
 
111
352
  Returns:
112
353
  The first row of the query results.
113
354
  """
114
355
  connection = self._connection(connection)
115
- sql, parameters = self._process_sql_params(sql, parameters)
116
-
117
- result = await connection.fetchrow(sql, *parameters) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
118
- result = self.check_not_found(result)
356
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
357
+ parameters = parameters if parameters is not None else {}
358
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
359
+ if result is None:
360
+ return None
119
361
  if schema_type is None:
120
362
  # Always return as dictionary
121
- return dict(result.items()) # type: ignore[attr-defined]
122
- return cast("ModelDTOT", schema_type(**dict(result.items()))) # type: ignore[attr-defined]
363
+ return dict(result.items())
364
+ return cast("ModelDTOT", schema_type(**dict(result.items())))
123
365
 
366
+ @overload
367
+ async def select_value(
368
+ self,
369
+ sql: str,
370
+ parameters: "Optional[StatementParameterType]" = None,
371
+ /,
372
+ *,
373
+ connection: "Optional[AsyncpgConnection]" = None,
374
+ schema_type: None = None,
375
+ **kwargs: Any,
376
+ ) -> "Any": ...
377
+ @overload
378
+ async def select_value(
379
+ self,
380
+ sql: str,
381
+ parameters: "Optional[StatementParameterType]" = None,
382
+ /,
383
+ *,
384
+ connection: "Optional[AsyncpgConnection]" = None,
385
+ schema_type: "type[T]",
386
+ **kwargs: Any,
387
+ ) -> "T": ...
124
388
  async def select_value(
125
389
  self,
126
390
  sql: str,
127
391
  parameters: "Optional[StatementParameterType]" = None,
128
392
  /,
129
- connection: "Optional[PgConnection]" = None,
393
+ *,
394
+ connection: "Optional[AsyncpgConnection]" = None,
130
395
  schema_type: "Optional[type[T]]" = None,
396
+ **kwargs: Any,
131
397
  ) -> "Union[T, Any]":
132
398
  """Fetch a single value from the database.
133
399
 
400
+ Args:
401
+ sql: SQL statement.
402
+ parameters: Query parameters.
403
+ connection: Optional connection to use.
404
+ schema_type: Optional schema class for the result.
405
+ **kwargs: Additional keyword arguments.
406
+
134
407
  Returns:
135
408
  The first value from the first row of results, or None if no results.
136
409
  """
137
410
  connection = self._connection(connection)
138
- sql, params = self._process_sql_params(sql, parameters)
139
- # Use empty tuple if params is None
140
- params = params if params is not None else ()
141
-
142
- result = await connection.fetchval(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
411
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
412
+ parameters = parameters if parameters is not None else {}
413
+ result = await connection.fetchval(sql, *parameters) # pyright: ignore
143
414
  result = self.check_not_found(result)
144
415
  if schema_type is None:
145
- return result[0]
146
- return schema_type(result[0]) # type: ignore[call-arg]
416
+ return result
417
+ return schema_type(result) # type: ignore[call-arg]
147
418
 
419
+ @overload
148
420
  async def select_value_or_none(
149
421
  self,
150
422
  sql: str,
151
423
  parameters: "Optional[StatementParameterType]" = None,
152
424
  /,
153
- connection: "Optional[PgConnection]" = None,
425
+ *,
426
+ connection: "Optional[AsyncpgConnection]" = None,
427
+ schema_type: None = None,
428
+ **kwargs: Any,
429
+ ) -> "Optional[Any]": ...
430
+ @overload
431
+ async def select_value_or_none(
432
+ self,
433
+ sql: str,
434
+ parameters: "Optional[StatementParameterType]" = None,
435
+ /,
436
+ *,
437
+ connection: "Optional[AsyncpgConnection]" = None,
438
+ schema_type: "type[T]",
439
+ **kwargs: Any,
440
+ ) -> "Optional[T]": ...
441
+ async def select_value_or_none(
442
+ self,
443
+ sql: str,
444
+ parameters: "Optional[StatementParameterType]" = None,
445
+ /,
446
+ *,
447
+ connection: "Optional[AsyncpgConnection]" = None,
154
448
  schema_type: "Optional[type[T]]" = None,
449
+ **kwargs: Any,
155
450
  ) -> "Optional[Union[T, Any]]":
156
451
  """Fetch a single value from the database.
157
452
 
@@ -159,23 +454,23 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
159
454
  The first value from the first row of results, or None if no results.
160
455
  """
161
456
  connection = self._connection(connection)
162
- sql, params = self._process_sql_params(sql, parameters)
163
- # Use empty tuple if params is None
164
- params = params if params is not None else ()
165
-
166
- result = await connection.fetchval(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
457
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
458
+ parameters = parameters if parameters is not None else {}
459
+ result = await connection.fetchval(sql, *parameters) # pyright: ignore
167
460
  if result is None:
168
461
  return None
169
462
  if schema_type is None:
170
- return result[0]
171
- return schema_type(result[0]) # type: ignore[call-arg]
463
+ return result
464
+ return schema_type(result) # type: ignore[call-arg]
172
465
 
173
466
  async def insert_update_delete(
174
467
  self,
175
468
  sql: str,
176
469
  parameters: Optional["StatementParameterType"] = None,
177
470
  /,
178
- connection: Optional["PgConnection"] = None,
471
+ *,
472
+ connection: Optional["AsyncpgConnection"] = None,
473
+ **kwargs: Any,
179
474
  ) -> int:
180
475
  """Insert, update, or delete data from the database.
181
476
 
@@ -183,47 +478,69 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
183
478
  sql: SQL statement.
184
479
  parameters: Query parameters.
185
480
  connection: Optional connection to use.
481
+ **kwargs: Additional keyword arguments.
186
482
 
187
483
  Returns:
188
484
  Row count affected by the operation.
189
485
  """
190
486
  connection = self._connection(connection)
191
- sql, params = self._process_sql_params(sql, parameters)
192
- # Use empty tuple if params is None
193
- params = params if params is not None else ()
194
-
195
- status = await connection.execute(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
487
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
488
+ parameters = parameters if parameters is not None else {}
489
+ status = await connection.execute(sql, *parameters) # pyright: ignore
196
490
  # AsyncPG returns a string like "INSERT 0 1" where the last number is the affected rows
197
491
  try:
198
492
  return int(status.split()[-1]) # pyright: ignore[reportUnknownMemberType]
199
493
  except (ValueError, IndexError, AttributeError):
200
494
  return -1 # Fallback if we can't parse the status
201
495
 
496
+ @overload
497
+ async def insert_update_delete_returning(
498
+ self,
499
+ sql: str,
500
+ parameters: "Optional[StatementParameterType]" = None,
501
+ /,
502
+ *,
503
+ connection: "Optional[AsyncpgConnection]" = None,
504
+ schema_type: None = None,
505
+ **kwargs: Any,
506
+ ) -> "dict[str, Any]": ...
507
+ @overload
508
+ async def insert_update_delete_returning(
509
+ self,
510
+ sql: str,
511
+ parameters: "Optional[StatementParameterType]" = None,
512
+ /,
513
+ *,
514
+ connection: "Optional[AsyncpgConnection]" = None,
515
+ schema_type: "type[ModelDTOT]",
516
+ **kwargs: Any,
517
+ ) -> "ModelDTOT": ...
202
518
  async def insert_update_delete_returning(
203
519
  self,
204
520
  sql: str,
205
521
  parameters: Optional["StatementParameterType"] = None,
206
522
  /,
207
- connection: Optional["PgConnection"] = None,
523
+ *,
524
+ connection: Optional["AsyncpgConnection"] = None,
208
525
  schema_type: "Optional[type[ModelDTOT]]" = None,
526
+ **kwargs: Any,
209
527
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
210
- """Insert, update, or delete data from the database and return result.
528
+ """Insert, update, or delete data from the database and return the affected row.
211
529
 
212
530
  Args:
213
531
  sql: SQL statement.
214
532
  parameters: Query parameters.
215
533
  connection: Optional connection to use.
216
534
  schema_type: Optional schema class for the result.
535
+ **kwargs: Additional keyword arguments.
217
536
 
218
537
  Returns:
219
- The first row of results.
538
+ The affected row data as either a model instance or dictionary.
220
539
  """
221
540
  connection = self._connection(connection)
222
- sql, params = self._process_sql_params(sql, parameters)
223
- # Use empty tuple if params is None
224
- params = params if params is not None else ()
225
-
226
- result = await connection.fetchrow(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
541
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
542
+ parameters = parameters if parameters is not None else {}
543
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
227
544
  if result is None:
228
545
  return None
229
546
  if schema_type is None:
@@ -236,7 +553,9 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
236
553
  sql: str,
237
554
  parameters: Optional["StatementParameterType"] = None,
238
555
  /,
239
- connection: Optional["PgConnection"] = None,
556
+ *,
557
+ connection: Optional["AsyncpgConnection"] = None,
558
+ **kwargs: Any,
240
559
  ) -> str:
241
560
  """Execute a script.
242
561
 
@@ -244,45 +563,16 @@ class AsyncpgDriver(AsyncDriverAdapterProtocol["PgConnection"]):
244
563
  sql: SQL statement.
245
564
  parameters: Query parameters.
246
565
  connection: Optional connection to use.
566
+ **kwargs: Additional keyword arguments.
247
567
 
248
568
  Returns:
249
569
  Status message for the operation.
250
570
  """
251
571
  connection = self._connection(connection)
252
- sql, params = self._process_sql_params(sql, parameters)
253
- # Use empty tuple if params is None
254
- params = params if params is not None else ()
255
-
256
- return await connection.execute(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
257
-
258
- async def execute_script_returning(
259
- self,
260
- sql: str,
261
- parameters: Optional["StatementParameterType"] = None,
262
- /,
263
- connection: Optional["PgConnection"] = None,
264
- schema_type: "Optional[type[ModelDTOT]]" = None,
265
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
266
- """Execute a script and return result.
572
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
573
+ parameters = parameters if parameters is not None else {}
574
+ return await connection.execute(sql, *parameters) # pyright: ignore
267
575
 
268
- Args:
269
- sql: SQL statement.
270
- parameters: Query parameters.
271
- connection: Optional connection to use.
272
- schema_type: Optional schema class for the result.
273
-
274
- Returns:
275
- The first row of results.
276
- """
277
- connection = self._connection(connection)
278
- sql, params = self._process_sql_params(sql, parameters)
279
- # Use empty tuple if params is None
280
- params = params if params is not None else ()
281
-
282
- result = await connection.fetchrow(sql, *params) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
283
- if result is None:
284
- return None
285
- if schema_type is None:
286
- # Always return as dictionary
287
- return dict(result.items()) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
288
- return cast("ModelDTOT", schema_type(**dict(result.items()))) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType, reportUnknownVariableType]
576
+ def _connection(self, connection: Optional["AsyncpgConnection"] = None) -> "AsyncpgConnection":
577
+ """Return the connection to use. If None, use the default connection."""
578
+ return connection if connection is not None else self.connection
@@ -1,7 +1,7 @@
1
- from sqlspec.adapters.duckdb.config import DuckDB
1
+ from sqlspec.adapters.duckdb.config import DuckDBConfig
2
2
  from sqlspec.adapters.duckdb.driver import DuckDBDriver
3
3
 
4
4
  __all__ = (
5
- "DuckDB",
5
+ "DuckDBConfig",
6
6
  "DuckDBDriver",
7
7
  )