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

@@ -1,18 +1,21 @@
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.filters import StatementFilter
12
+ from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
13
+ from sqlspec.statement import SQLStatement
12
14
 
13
15
  if TYPE_CHECKING:
14
- from collections.abc import Sequence
16
+ from collections.abc import Mapping, Sequence
15
17
 
18
+ from asyncpg import Record
16
19
  from asyncpg.connection import Connection
17
20
  from asyncpg.pool import PoolConnectionProxy
18
21
 
@@ -22,23 +25,37 @@ __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,167 +65,116 @@ 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
+ *filters: "StatementFilter",
56
73
  **kwargs: Any,
57
74
  ) -> "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
75
+ """Process SQL and parameters for AsyncPG using SQLStatement.
97
76
 
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
77
+ This method applies filters (if provided), processes the SQL through SQLStatement
78
+ with dialect support, and converts parameters to the format required by AsyncPG.
79
+
80
+ Args:
81
+ sql: SQL statement.
82
+ parameters: Query parameters. Can be data or a StatementFilter.
83
+ *filters: Statement filters to apply.
84
+ **kwargs: Additional keyword arguments.
149
85
 
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
86
+ Returns:
87
+ Tuple of processed SQL and parameters.
88
+ """
89
+ data_params_for_statement: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
90
+ combined_filters_list: list[StatementFilter] = list(filters)
91
+
92
+ if parameters is not None:
93
+ if isinstance(parameters, StatementFilter):
94
+ combined_filters_list.insert(0, parameters)
95
+ # data_params_for_statement remains None
96
+ else:
97
+ # If parameters is not a StatementFilter, it's actual data parameters.
98
+ data_params_for_statement = parameters
99
+
100
+ # Handle scalar parameter by converting to a single-item tuple if it's data
101
+ if data_params_for_statement is not None and not isinstance(data_params_for_statement, (list, tuple, dict)):
102
+ data_params_for_statement = (data_params_for_statement,)
103
+
104
+ # Create a SQLStatement with PostgreSQL dialect
105
+ statement = SQLStatement(sql, data_params_for_statement, kwargs=kwargs, dialect=self.dialect)
106
+
107
+ # Apply any filters from the combined list
108
+ for filter_obj in combined_filters_list:
109
+ statement = statement.apply_filter(filter_obj)
110
+
111
+ # Process the statement
112
+ processed_sql, processed_params, parsed_expr = statement.process()
113
+
114
+ if processed_params is None:
115
+ return processed_sql, ()
116
+
117
+ # Convert question marks to PostgreSQL style $N parameters
118
+ if isinstance(processed_params, (list, tuple)) and "?" in processed_sql:
119
+ # Use a counter to generate $1, $2, etc. for each ? in the SQL that's outside strings/comments
120
+ param_index = 0
121
+
122
+ def replace_question_mark(match: Match[str]) -> str:
123
+ # Only process the match if it's not in a skipped context (string/comment)
124
+ if match.group(1): # This is a question mark outside string/comment
125
+ nonlocal param_index
126
+ param_index += 1
127
+ return f"${param_index}"
128
+ # Return the entire matched text unchanged for strings/comments
129
+ return match.group(0)
130
+
131
+ processed_sql = QUESTION_MARK_PATTERN.sub(replace_question_mark, processed_sql)
132
+
133
+ # Now handle the asyncpg-specific parameter conversion - asyncpg requires positional parameters
134
+ if isinstance(processed_params, dict):
135
+ if parsed_expr is not None:
136
+ # Find named parameters
137
+ named_params = []
138
+ for node in parsed_expr.find_all(exp.Parameter, exp.Placeholder):
139
+ if isinstance(node, exp.Parameter) and node.name and node.name in processed_params:
140
+ named_params.append(node.name)
141
+ elif (
142
+ isinstance(node, exp.Placeholder)
143
+ and isinstance(node.this, str)
144
+ and node.this in processed_params
145
+ ):
146
+ named_params.append(node.this)
147
+
148
+ # Convert named parameters to positional
149
+ if named_params:
150
+ # Transform the SQL to use $1, $2, etc.
151
+ def replace_named_with_positional(node: exp.Expression) -> exp.Expression:
152
+ if isinstance(node, exp.Parameter) and node.name and node.name in processed_params:
153
+ idx = named_params.index(node.name) + 1
154
+ return exp.Parameter(this=str(idx))
155
+ if (
156
+ isinstance(node, exp.Placeholder)
157
+ and isinstance(node.this, str)
158
+ and node.this in processed_params
159
+ ):
160
+ idx = named_params.index(node.this) + 1
161
+ return exp.Parameter(this=str(idx))
162
+ return node
163
+
164
+ return parsed_expr.transform(replace_named_with_positional, copy=True).sql(
165
+ dialect=self.dialect
166
+ ), tuple(processed_params[name] for name in named_params)
167
+ return processed_sql, tuple(processed_params.values())
168
+ if isinstance(processed_params, (list, tuple)):
169
+ return processed_sql, tuple(processed_params)
170
+ return processed_sql, (processed_params,) # type: ignore[unreachable]
204
171
 
205
172
  @overload
206
173
  async def select(
207
174
  self,
208
175
  sql: str,
209
176
  parameters: "Optional[StatementParameterType]" = None,
210
- /,
211
- *,
177
+ *filters: "StatementFilter",
212
178
  connection: "Optional[AsyncpgConnection]" = None,
213
179
  schema_type: None = None,
214
180
  **kwargs: Any,
@@ -218,8 +184,7 @@ class AsyncpgDriver(
218
184
  self,
219
185
  sql: str,
220
186
  parameters: "Optional[StatementParameterType]" = None,
221
- /,
222
- *,
187
+ *filters: "StatementFilter",
223
188
  connection: "Optional[AsyncpgConnection]" = None,
224
189
  schema_type: "type[ModelDTOT]",
225
190
  **kwargs: Any,
@@ -227,18 +192,18 @@ class AsyncpgDriver(
227
192
  async def select(
228
193
  self,
229
194
  sql: str,
230
- parameters: Optional["StatementParameterType"] = None,
231
- /,
232
- *,
233
- connection: Optional["AsyncpgConnection"] = None,
195
+ parameters: "Optional[StatementParameterType]" = None,
196
+ *filters: "StatementFilter",
197
+ connection: "Optional[AsyncpgConnection]" = None,
234
198
  schema_type: "Optional[type[ModelDTOT]]" = None,
235
199
  **kwargs: Any,
236
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
200
+ ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
237
201
  """Fetch data from the database.
238
202
 
239
203
  Args:
240
204
  sql: SQL statement.
241
- parameters: Query parameters.
205
+ parameters: Query parameters. Can be data or a StatementFilter.
206
+ *filters: Statement filters to apply.
242
207
  connection: Optional connection to use.
243
208
  schema_type: Optional schema class for the result.
244
209
  **kwargs: Additional keyword arguments.
@@ -247,23 +212,20 @@ class AsyncpgDriver(
247
212
  List of row data as either model instances or dictionaries.
248
213
  """
249
214
  connection = self._connection(connection)
250
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
251
- parameters = parameters if parameters is not None else {}
215
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
216
+ parameters = parameters if parameters is not None else ()
252
217
 
253
218
  results = await connection.fetch(sql, *parameters) # pyright: ignore
254
219
  if not results:
255
220
  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]
221
+ return self.to_schema([dict(row.items()) for row in results], schema_type=schema_type) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
259
222
 
260
223
  @overload
261
224
  async def select_one(
262
225
  self,
263
226
  sql: str,
264
227
  parameters: "Optional[StatementParameterType]" = None,
265
- /,
266
- *,
228
+ *filters: "StatementFilter",
267
229
  connection: "Optional[AsyncpgConnection]" = None,
268
230
  schema_type: None = None,
269
231
  **kwargs: Any,
@@ -273,8 +235,7 @@ class AsyncpgDriver(
273
235
  self,
274
236
  sql: str,
275
237
  parameters: "Optional[StatementParameterType]" = None,
276
- /,
277
- *,
238
+ *filters: "StatementFilter",
278
239
  connection: "Optional[AsyncpgConnection]" = None,
279
240
  schema_type: "type[ModelDTOT]",
280
241
  **kwargs: Any,
@@ -282,18 +243,18 @@ class AsyncpgDriver(
282
243
  async def select_one(
283
244
  self,
284
245
  sql: str,
285
- parameters: Optional["StatementParameterType"] = None,
286
- /,
287
- *,
288
- connection: Optional["AsyncpgConnection"] = None,
246
+ parameters: "Optional[StatementParameterType]" = None,
247
+ *filters: "StatementFilter",
248
+ connection: "Optional[AsyncpgConnection]" = None,
289
249
  schema_type: "Optional[type[ModelDTOT]]" = None,
290
250
  **kwargs: Any,
291
- ) -> "Union[ModelDTOT, dict[str, Any]]":
251
+ ) -> "Union[dict[str, Any], ModelDTOT]":
292
252
  """Fetch one row from the database.
293
253
 
294
254
  Args:
295
255
  sql: SQL statement.
296
- parameters: Query parameters.
256
+ parameters: Query parameters. Can be data or a StatementFilter.
257
+ *filters: Statement filters to apply.
297
258
  connection: Optional connection to use.
298
259
  schema_type: Optional schema class for the result.
299
260
  **kwargs: Additional keyword arguments.
@@ -302,23 +263,18 @@ class AsyncpgDriver(
302
263
  The first row of the query results.
303
264
  """
304
265
  connection = self._connection(connection)
305
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
306
- parameters = parameters if parameters is not None else {}
266
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
267
+ parameters = parameters if parameters is not None else ()
307
268
  result = await connection.fetchrow(sql, *parameters) # pyright: ignore
308
269
  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]
270
+ return self.to_schema(dict(result.items()), schema_type=schema_type)
314
271
 
315
272
  @overload
316
273
  async def select_one_or_none(
317
274
  self,
318
275
  sql: str,
319
276
  parameters: "Optional[StatementParameterType]" = None,
320
- /,
321
- *,
277
+ *filters: "StatementFilter",
322
278
  connection: "Optional[AsyncpgConnection]" = None,
323
279
  schema_type: None = None,
324
280
  **kwargs: Any,
@@ -328,8 +284,7 @@ class AsyncpgDriver(
328
284
  self,
329
285
  sql: str,
330
286
  parameters: "Optional[StatementParameterType]" = None,
331
- /,
332
- *,
287
+ *filters: "StatementFilter",
333
288
  connection: "Optional[AsyncpgConnection]" = None,
334
289
  schema_type: "type[ModelDTOT]",
335
290
  **kwargs: Any,
@@ -337,18 +292,18 @@ class AsyncpgDriver(
337
292
  async def select_one_or_none(
338
293
  self,
339
294
  sql: str,
340
- parameters: Optional["StatementParameterType"] = None,
341
- /,
342
- *,
343
- connection: Optional["AsyncpgConnection"] = None,
295
+ parameters: "Optional[StatementParameterType]" = None,
296
+ *filters: "StatementFilter",
297
+ connection: "Optional[AsyncpgConnection]" = None,
344
298
  schema_type: "Optional[type[ModelDTOT]]" = None,
345
299
  **kwargs: Any,
346
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
300
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
347
301
  """Fetch one row from the database.
348
302
 
349
303
  Args:
350
304
  sql: SQL statement.
351
- parameters: Query parameters.
305
+ parameters: Query parameters. Can be data or a StatementFilter.
306
+ *filters: Statement filters to apply.
352
307
  connection: Optional connection to use.
353
308
  schema_type: Optional schema class for the result.
354
309
  **kwargs: Additional keyword arguments.
@@ -357,23 +312,19 @@ class AsyncpgDriver(
357
312
  The first row of the query results.
358
313
  """
359
314
  connection = self._connection(connection)
360
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
361
- parameters = parameters if parameters is not None else {}
315
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
316
+ parameters = parameters if parameters is not None else ()
362
317
  result = await connection.fetchrow(sql, *parameters) # pyright: ignore
363
318
  if result is None:
364
319
  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())))
320
+ return self.to_schema(dict(result.items()), schema_type=schema_type)
369
321
 
370
322
  @overload
371
323
  async def select_value(
372
324
  self,
373
325
  sql: str,
374
326
  parameters: "Optional[StatementParameterType]" = None,
375
- /,
376
- *,
327
+ *filters: "StatementFilter",
377
328
  connection: "Optional[AsyncpgConnection]" = None,
378
329
  schema_type: None = None,
379
330
  **kwargs: Any,
@@ -383,8 +334,7 @@ class AsyncpgDriver(
383
334
  self,
384
335
  sql: str,
385
336
  parameters: "Optional[StatementParameterType]" = None,
386
- /,
387
- *,
337
+ *filters: "StatementFilter",
388
338
  connection: "Optional[AsyncpgConnection]" = None,
389
339
  schema_type: "type[T]",
390
340
  **kwargs: Any,
@@ -393,8 +343,7 @@ class AsyncpgDriver(
393
343
  self,
394
344
  sql: str,
395
345
  parameters: "Optional[StatementParameterType]" = None,
396
- /,
397
- *,
346
+ *filters: "StatementFilter",
398
347
  connection: "Optional[AsyncpgConnection]" = None,
399
348
  schema_type: "Optional[type[T]]" = None,
400
349
  **kwargs: Any,
@@ -403,7 +352,8 @@ class AsyncpgDriver(
403
352
 
404
353
  Args:
405
354
  sql: SQL statement.
406
- parameters: Query parameters.
355
+ parameters: Query parameters. Can be data or a StatementFilter.
356
+ *filters: Statement filters to apply.
407
357
  connection: Optional connection to use.
408
358
  schema_type: Optional schema class for the result.
409
359
  **kwargs: Additional keyword arguments.
@@ -412,8 +362,8 @@ class AsyncpgDriver(
412
362
  The first value from the first row of results, or None if no results.
413
363
  """
414
364
  connection = self._connection(connection)
415
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
416
- parameters = parameters if parameters is not None else {}
365
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
366
+ parameters = parameters if parameters is not None else ()
417
367
  result = await connection.fetchval(sql, *parameters) # pyright: ignore
418
368
  result = self.check_not_found(result)
419
369
  if schema_type is None:
@@ -425,8 +375,7 @@ class AsyncpgDriver(
425
375
  self,
426
376
  sql: str,
427
377
  parameters: "Optional[StatementParameterType]" = None,
428
- /,
429
- *,
378
+ *filters: "StatementFilter",
430
379
  connection: "Optional[AsyncpgConnection]" = None,
431
380
  schema_type: None = None,
432
381
  **kwargs: Any,
@@ -436,8 +385,7 @@ class AsyncpgDriver(
436
385
  self,
437
386
  sql: str,
438
387
  parameters: "Optional[StatementParameterType]" = None,
439
- /,
440
- *,
388
+ *filters: "StatementFilter",
441
389
  connection: "Optional[AsyncpgConnection]" = None,
442
390
  schema_type: "type[T]",
443
391
  **kwargs: Any,
@@ -446,20 +394,27 @@ class AsyncpgDriver(
446
394
  self,
447
395
  sql: str,
448
396
  parameters: "Optional[StatementParameterType]" = None,
449
- /,
450
- *,
397
+ *filters: "StatementFilter",
451
398
  connection: "Optional[AsyncpgConnection]" = None,
452
399
  schema_type: "Optional[type[T]]" = None,
453
400
  **kwargs: Any,
454
401
  ) -> "Optional[Union[T, Any]]":
455
402
  """Fetch a single value from the database.
456
403
 
404
+ Args:
405
+ sql: SQL statement.
406
+ parameters: Query parameters. Can be data or a StatementFilter.
407
+ *filters: Statement filters to apply.
408
+ connection: Optional connection to use.
409
+ schema_type: Optional schema class for the result.
410
+ **kwargs: Additional keyword arguments.
411
+
457
412
  Returns:
458
413
  The first value from the first row of results, or None if no results.
459
414
  """
460
415
  connection = self._connection(connection)
461
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
462
- parameters = parameters if parameters is not None else {}
416
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
417
+ parameters = parameters if parameters is not None else ()
463
418
  result = await connection.fetchval(sql, *parameters) # pyright: ignore
464
419
  if result is None:
465
420
  return None
@@ -470,9 +425,8 @@ class AsyncpgDriver(
470
425
  async def insert_update_delete(
471
426
  self,
472
427
  sql: str,
473
- parameters: Optional["StatementParameterType"] = None,
474
- /,
475
- *,
428
+ parameters: "Optional[StatementParameterType]" = None,
429
+ *filters: "StatementFilter",
476
430
  connection: Optional["AsyncpgConnection"] = None,
477
431
  **kwargs: Any,
478
432
  ) -> int:
@@ -480,7 +434,8 @@ class AsyncpgDriver(
480
434
 
481
435
  Args:
482
436
  sql: SQL statement.
483
- parameters: Query parameters.
437
+ parameters: Query parameters. Can be data or a StatementFilter.
438
+ *filters: Statement filters to apply.
484
439
  connection: Optional connection to use.
485
440
  **kwargs: Additional keyword arguments.
486
441
 
@@ -488,22 +443,21 @@ class AsyncpgDriver(
488
443
  Row count affected by the operation.
489
444
  """
490
445
  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
446
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
447
+ parameters = parameters if parameters is not None else ()
448
+ result = await connection.execute(sql, *parameters) # pyright: ignore
449
+ # asyncpg returns e.g. 'INSERT 0 1', 'UPDATE 0 2', etc.
450
+ match = ROWCOUNT_REGEX.match(result)
451
+ if match:
452
+ return int(match.group(1))
453
+ return 0
499
454
 
500
455
  @overload
501
456
  async def insert_update_delete_returning(
502
457
  self,
503
458
  sql: str,
504
459
  parameters: "Optional[StatementParameterType]" = None,
505
- /,
506
- *,
460
+ *filters: "StatementFilter",
507
461
  connection: "Optional[AsyncpgConnection]" = None,
508
462
  schema_type: None = None,
509
463
  **kwargs: Any,
@@ -513,8 +467,7 @@ class AsyncpgDriver(
513
467
  self,
514
468
  sql: str,
515
469
  parameters: "Optional[StatementParameterType]" = None,
516
- /,
517
- *,
470
+ *filters: "StatementFilter",
518
471
  connection: "Optional[AsyncpgConnection]" = None,
519
472
  schema_type: "type[ModelDTOT]",
520
473
  **kwargs: Any,
@@ -522,10 +475,9 @@ class AsyncpgDriver(
522
475
  async def insert_update_delete_returning(
523
476
  self,
524
477
  sql: str,
525
- parameters: Optional["StatementParameterType"] = None,
526
- /,
527
- *,
528
- connection: Optional["AsyncpgConnection"] = None,
478
+ parameters: "Optional[StatementParameterType]" = None,
479
+ *filters: "StatementFilter",
480
+ connection: "Optional[AsyncpgConnection]" = None,
529
481
  schema_type: "Optional[type[ModelDTOT]]" = None,
530
482
  **kwargs: Any,
531
483
  ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
@@ -533,7 +485,8 @@ class AsyncpgDriver(
533
485
 
534
486
  Args:
535
487
  sql: SQL statement.
536
- parameters: Query parameters.
488
+ parameters: Query parameters. Can be data or a StatementFilter.
489
+ *filters: Statement filters to apply.
537
490
  connection: Optional connection to use.
538
491
  schema_type: Optional schema class for the result.
539
492
  **kwargs: Additional keyword arguments.
@@ -542,23 +495,19 @@ class AsyncpgDriver(
542
495
  The affected row data as either a model instance or dictionary.
543
496
  """
544
497
  connection = self._connection(connection)
545
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
546
- parameters = parameters if parameters is not None else {}
498
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
499
+ parameters = parameters if parameters is not None else ()
547
500
  result = await connection.fetchrow(sql, *parameters) # pyright: ignore
548
501
  if result is None:
549
502
  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]
503
+
504
+ return self.to_schema(dict(result.items()), schema_type=schema_type)
554
505
 
555
506
  async def execute_script(
556
507
  self,
557
508
  sql: str,
558
- parameters: Optional["StatementParameterType"] = None,
559
- /,
560
- *,
561
- connection: Optional["AsyncpgConnection"] = None,
509
+ parameters: "Optional[StatementParameterType]" = None,
510
+ connection: "Optional[AsyncpgConnection]" = None,
562
511
  **kwargs: Any,
563
512
  ) -> str:
564
513
  """Execute a script.
@@ -574,9 +523,9 @@ class AsyncpgDriver(
574
523
  """
575
524
  connection = self._connection(connection)
576
525
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
577
- parameters = parameters if parameters is not None else {}
526
+ parameters = parameters if parameters is not None else ()
578
527
  return await connection.execute(sql, *parameters) # pyright: ignore
579
528
 
580
- def _connection(self, connection: Optional["AsyncpgConnection"] = None) -> "AsyncpgConnection":
529
+ def _connection(self, connection: "Optional[AsyncpgConnection]" = None) -> "AsyncpgConnection":
581
530
  """Return the connection to use. If None, use the default connection."""
582
531
  return connection if connection is not None else self.connection