sqlspec 0.10.0__py3-none-any.whl → 0.11.0__py3-none-any.whl

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

Potentially problematic release.


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

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