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,20 +1,23 @@
1
- # ruff: noqa: PLR0915, PLR0914, PLR0912, C901
2
1
  """Psqlpy Driver Implementation."""
3
2
 
4
3
  import logging
5
4
  import re
6
- from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
5
+ from re import Match
6
+ from typing import TYPE_CHECKING, Any, Optional, Union, overload
7
7
 
8
8
  from psqlpy import Connection, QueryResult
9
9
  from psqlpy.exceptions import RustPSQLDriverPyBaseError
10
+ from sqlglot import exp
10
11
 
11
12
  from sqlspec.base import AsyncDriverAdapterProtocol
12
13
  from sqlspec.exceptions import SQLParsingError
13
- from sqlspec.mixins import SQLTranslatorMixin
14
- from sqlspec.statement import PARAM_REGEX, SQLStatement
14
+ from sqlspec.filters import StatementFilter
15
+ from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
16
+ from sqlspec.statement import SQLStatement
17
+ from sqlspec.typing import is_dict
15
18
 
16
19
  if TYPE_CHECKING:
17
- from collections.abc import Sequence
20
+ from collections.abc import Mapping, Sequence
18
21
 
19
22
  from psqlpy import QueryResult
20
23
 
@@ -22,23 +25,32 @@ if TYPE_CHECKING:
22
25
 
23
26
  __all__ = ("PsqlpyConnection", "PsqlpyDriver")
24
27
 
25
-
26
- PsqlpyConnection = Connection
27
- # Regex to find '?' placeholders, skipping those inside quotes or SQL comments
28
- QMARK_REGEX = re.compile(
29
- r"""(?P<dquote>"[^"]*") | # Double-quoted strings
30
- (?P<squote>\'[^\']*\') | # Single-quoted strings
31
- (?P<comment>--[^\n]*|/\*.*?\*/) | # SQL comments (single/multi-line)
32
- (?P<qmark>\?) # The question mark placeholder
33
- """,
28
+ # Improved regex to match question mark placeholders only when they are outside string literals and comments
29
+ # This pattern handles:
30
+ # 1. Single quoted strings with escaped quotes
31
+ # 2. Double quoted strings with escaped quotes
32
+ # 3. Single-line comments (-- to end of line)
33
+ # 4. Multi-line comments (/* to */)
34
+ # 5. Only question marks outside of these contexts are considered parameters
35
+ QUESTION_MARK_PATTERN = re.compile(
36
+ r"""
37
+ (?:'[^']*(?:''[^']*)*') | # Skip single-quoted strings (with '' escapes)
38
+ (?:"[^"]*(?:""[^"]*)*") | # Skip double-quoted strings (with "" escapes)
39
+ (?:--.*?(?:\n|$)) | # Skip single-line comments
40
+ (?:/\*(?:[^*]|\*(?!/))*\*/) | # Skip multi-line comments
41
+ (\?) # Capture only question marks outside of these contexts
42
+ """,
34
43
  re.VERBOSE | re.DOTALL,
35
44
  )
45
+
46
+ PsqlpyConnection = Connection
36
47
  logger = logging.getLogger("sqlspec")
37
48
 
38
49
 
39
50
  class PsqlpyDriver(
40
51
  SQLTranslatorMixin["PsqlpyConnection"],
41
52
  AsyncDriverAdapterProtocol["PsqlpyConnection"],
53
+ ResultConverter,
42
54
  ):
43
55
  """Psqlpy Postgres Driver Adapter."""
44
56
 
@@ -52,126 +64,100 @@ class PsqlpyDriver(
52
64
  self,
53
65
  sql: str,
54
66
  parameters: "Optional[StatementParameterType]" = None,
55
- /,
67
+ *filters: "StatementFilter",
56
68
  **kwargs: Any,
57
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
69
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
58
70
  """Process SQL and parameters for psqlpy.
59
71
 
60
- psqlpy uses $1, $2 style parameters natively.
61
- This method converts '?' (tuple/list) and ':name' (dict) styles to $n.
62
- It relies on SQLStatement for initial parameter validation and merging.
63
-
64
72
  Args:
65
- sql: The SQL to process.
66
- parameters: The parameters to process.
67
- kwargs: Additional keyword arguments.
68
-
69
- Raises:
70
- SQLParsingError: If the SQL is invalid.
73
+ sql: SQL statement.
74
+ parameters: Query parameters.
75
+ *filters: Statement filters to apply.
76
+ **kwargs: Additional keyword arguments.
71
77
 
72
78
  Returns:
73
- A tuple of the processed SQL and parameters.
79
+ The SQL statement and parameters.
80
+
81
+ Raises:
82
+ SQLParsingError: If the SQL parsing fails.
74
83
  """
75
- stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
76
- sql, parameters = stmt.process()
77
-
78
- # Case 1: Parameters are a dictionary
79
- if isinstance(parameters, dict):
80
- processed_sql_parts: list[str] = []
81
- ordered_params = []
82
- last_end = 0
83
- param_index = 1
84
- found_params_regex: list[str] = []
85
-
86
- for match in PARAM_REGEX.finditer(sql):
87
- if match.group("dquote") or match.group("squote") or match.group("comment"):
88
- continue
89
-
90
- if match.group("var_name"): # Finds :var_name
91
- var_name = match.group("var_name")
92
- found_params_regex.append(var_name)
93
- start = match.start("var_name") - 1
94
- end = match.end("var_name")
95
-
96
- if var_name not in parameters:
97
- msg = f"Named parameter ':{var_name}' missing from parameters. SQL: {sql}"
98
- raise SQLParsingError(msg)
99
-
100
- processed_sql_parts.extend((sql[last_end:start], f"${param_index}"))
101
- ordered_params.append(parameters[var_name])
102
- last_end = end
84
+ data_params_for_statement: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
85
+ combined_filters_list: list[StatementFilter] = list(filters)
86
+
87
+ if parameters is not None:
88
+ if isinstance(parameters, StatementFilter):
89
+ combined_filters_list.insert(0, parameters)
90
+ else:
91
+ data_params_for_statement = parameters
92
+ if data_params_for_statement is not None and not isinstance(data_params_for_statement, (list, tuple, dict)):
93
+ data_params_for_statement = (data_params_for_statement,)
94
+ statement = SQLStatement(sql, data_params_for_statement, kwargs=kwargs, dialect=self.dialect)
95
+
96
+ for filter_obj in combined_filters_list:
97
+ statement = statement.apply_filter(filter_obj)
98
+
99
+ # Process the statement
100
+ sql, validated_params, parsed_expr = statement.process()
101
+
102
+ if validated_params is None:
103
+ return sql, None # psqlpy can handle None
104
+
105
+ # Convert positional parameters from question mark style to PostgreSQL's $N style
106
+ if isinstance(validated_params, (list, tuple)):
107
+ # Use a counter to generate $1, $2, etc. for each ? in the SQL that's outside strings/comments
108
+ param_index = 0
109
+
110
+ def replace_question_mark(match: Match[str]) -> str:
111
+ # Only process the match if it's not in a skipped context (string/comment)
112
+ if match.group(1): # This is a question mark outside string/comment
113
+ nonlocal param_index
103
114
  param_index += 1
104
-
105
- processed_sql_parts.append(sql[last_end:])
106
- final_sql = "".join(processed_sql_parts)
107
-
108
- if not found_params_regex and parameters:
109
- logger.warning(
110
- "Dict params provided (%s), but no :name placeholders found. SQL: %s",
111
- list(parameters.keys()),
112
- sql,
115
+ return f"${param_index}"
116
+ # Return the entire matched text unchanged for strings/comments
117
+ return match.group(0)
118
+
119
+ return QUESTION_MARK_PATTERN.sub(replace_question_mark, sql), tuple(validated_params)
120
+
121
+ # If no parsed expression is available, we can't safely transform dictionary parameters
122
+ if is_dict(validated_params) and parsed_expr is None:
123
+ msg = f"psqlpy: SQL parsing failed and dictionary parameters were provided. Cannot determine parameter order without successful parse. SQL: {sql}"
124
+ raise SQLParsingError(msg)
125
+
126
+ # Convert dictionary parameters to the format expected by psqlpy
127
+ if is_dict(validated_params) and parsed_expr is not None:
128
+ # Find all named parameters in the SQL expression
129
+ named_params = []
130
+
131
+ for node in parsed_expr.find_all(exp.Parameter, exp.Placeholder):
132
+ if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
133
+ named_params.append(node.name)
134
+ elif isinstance(node, exp.Placeholder) and isinstance(node.this, str) and node.this in validated_params:
135
+ named_params.append(node.this)
136
+
137
+ if named_params:
138
+ # Transform the SQL to use $1, $2, etc.
139
+ def convert_named_to_dollar(node: exp.Expression) -> exp.Expression:
140
+ if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
141
+ idx = named_params.index(node.name) + 1
142
+ return exp.Parameter(this=str(idx))
143
+ if (
144
+ isinstance(node, exp.Placeholder)
145
+ and isinstance(node.this, str)
146
+ and node.this in validated_params
147
+ ):
148
+ idx = named_params.index(node.this) + 1
149
+ return exp.Parameter(this=str(idx))
150
+ return node
151
+
152
+ return parsed_expr.transform(convert_named_to_dollar, copy=True).sql(dialect=self.dialect), tuple(
153
+ validated_params[name] for name in named_params
113
154
  )
114
- return sql, ()
115
-
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
- logger.warning("Unused parameters provided: %s. SQL: %s", unused_keys, sql)
121
-
122
- return final_sql, tuple(ordered_params)
123
-
124
- # Case 2: Parameters are a sequence/scalar
125
- if isinstance(parameters, (list, tuple)):
126
- sequence_processed_parts: list[str] = []
127
- param_index = 1
128
- last_end = 0
129
- qmark_found = False
130
-
131
- for match in QMARK_REGEX.finditer(sql):
132
- if match.group("dquote") or match.group("squote") or match.group("comment"):
133
- continue
134
-
135
- if match.group("qmark"):
136
- qmark_found = True
137
- start = match.start("qmark")
138
- end = match.end("qmark")
139
- sequence_processed_parts.extend((sql[last_end:start], f"${param_index}"))
140
- last_end = end
141
- param_index += 1
142
155
 
143
- sequence_processed_parts.append(sql[last_end:])
144
- final_sql = "".join(sequence_processed_parts)
145
-
146
- if parameters and not qmark_found:
147
- logger.warning("Sequence parameters provided, but no '?' placeholders found. SQL: %s", sql)
148
- return sql, parameters
149
-
150
- expected_params = param_index - 1
151
- actual_params = len(parameters)
152
- if expected_params != actual_params:
153
- msg = f"Parameter count mismatch: Expected {expected_params}, got {actual_params}. SQL: {final_sql}"
154
- raise SQLParsingError(msg)
155
-
156
- return final_sql, parameters
157
-
158
- # Case 3: Parameters are None
159
- if PARAM_REGEX.search(sql) or QMARK_REGEX.search(sql):
160
- # Perform a simpler check if any placeholders might exist if no params are given
161
- for match in PARAM_REGEX.finditer(sql):
162
- if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group(
163
- "var_name"
164
- ):
165
- msg = f"SQL contains named parameters (:name) but no parameters provided. SQL: {sql}"
166
- raise SQLParsingError(msg)
167
- for match in QMARK_REGEX.finditer(sql):
168
- if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group(
169
- "qmark"
170
- ):
171
- msg = f"SQL contains positional parameters (?) but no parameters provided. SQL: {sql}"
172
- raise SQLParsingError(msg)
173
-
174
- return sql, ()
156
+ # If no named parameters were found in the SQL but dictionary was provided
157
+ return sql, tuple(validated_params.values())
158
+
159
+ # For any other case, return validated params
160
+ return sql, (validated_params,) if not isinstance(validated_params, (list, tuple)) else tuple(validated_params) # type: ignore[unreachable]
175
161
 
176
162
  # --- Public API Methods --- #
177
163
  @overload
@@ -179,8 +165,7 @@ class PsqlpyDriver(
179
165
  self,
180
166
  sql: str,
181
167
  parameters: "Optional[StatementParameterType]" = None,
182
- /,
183
- *,
168
+ *filters: "StatementFilter",
184
169
  connection: "Optional[PsqlpyConnection]" = None,
185
170
  schema_type: None = None,
186
171
  **kwargs: Any,
@@ -190,8 +175,7 @@ class PsqlpyDriver(
190
175
  self,
191
176
  sql: str,
192
177
  parameters: "Optional[StatementParameterType]" = None,
193
- /,
194
- *,
178
+ *filters: "StatementFilter",
195
179
  connection: "Optional[PsqlpyConnection]" = None,
196
180
  schema_type: "type[ModelDTOT]",
197
181
  **kwargs: Any,
@@ -199,30 +183,41 @@ class PsqlpyDriver(
199
183
  async def select(
200
184
  self,
201
185
  sql: str,
202
- parameters: Optional["StatementParameterType"] = None,
203
- /,
204
- *,
205
- connection: Optional["PsqlpyConnection"] = None,
186
+ parameters: "Optional[StatementParameterType]" = None,
187
+ *filters: "StatementFilter",
188
+ connection: "Optional[PsqlpyConnection]" = None,
206
189
  schema_type: "Optional[type[ModelDTOT]]" = None,
207
190
  **kwargs: Any,
208
191
  ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
192
+ """Fetch data from the database.
193
+
194
+ Args:
195
+ sql: The SQL query string.
196
+ parameters: The parameters for the query (dict, tuple, list, or None).
197
+ *filters: Statement filters to apply.
198
+ connection: Optional connection override.
199
+ schema_type: Optional schema class for the result.
200
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
201
+
202
+ Returns:
203
+ List of row data as either model instances or dictionaries.
204
+ """
209
205
  connection = self._connection(connection)
210
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
206
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
211
207
  parameters = parameters or [] # psqlpy expects a list/tuple
212
208
 
213
209
  results: QueryResult = await connection.fetch(sql, parameters=parameters)
214
210
 
215
- if schema_type is None:
216
- return cast("list[dict[str, Any]]", results.result())
217
- return results.as_class(as_class=schema_type)
211
+ # Convert to dicts and use ResultConverter
212
+ dict_results = results.result()
213
+ return self.to_schema(dict_results, schema_type=schema_type)
218
214
 
219
215
  @overload
220
216
  async def select_one(
221
217
  self,
222
218
  sql: str,
223
219
  parameters: "Optional[StatementParameterType]" = None,
224
- /,
225
- *,
220
+ *filters: "StatementFilter",
226
221
  connection: "Optional[PsqlpyConnection]" = None,
227
222
  schema_type: None = None,
228
223
  **kwargs: Any,
@@ -232,8 +227,7 @@ class PsqlpyDriver(
232
227
  self,
233
228
  sql: str,
234
229
  parameters: "Optional[StatementParameterType]" = None,
235
- /,
236
- *,
230
+ *filters: "StatementFilter",
237
231
  connection: "Optional[PsqlpyConnection]" = None,
238
232
  schema_type: "type[ModelDTOT]",
239
233
  **kwargs: Any,
@@ -241,31 +235,44 @@ class PsqlpyDriver(
241
235
  async def select_one(
242
236
  self,
243
237
  sql: str,
244
- parameters: Optional["StatementParameterType"] = None,
245
- /,
246
- *,
247
- connection: Optional["PsqlpyConnection"] = None,
238
+ parameters: "Optional[StatementParameterType]" = None,
239
+ *filters: "StatementFilter",
240
+ connection: "Optional[PsqlpyConnection]" = None,
248
241
  schema_type: "Optional[type[ModelDTOT]]" = None,
249
242
  **kwargs: Any,
250
243
  ) -> "Union[ModelDTOT, dict[str, Any]]":
244
+ """Fetch one row from the database.
245
+
246
+ Args:
247
+ sql: The SQL query string.
248
+ parameters: The parameters for the query (dict, tuple, list, or None).
249
+ *filters: Statement filters to apply.
250
+ connection: Optional connection override.
251
+ schema_type: Optional schema class for the result.
252
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
253
+
254
+ Returns:
255
+ The first row of the query results.
256
+ """
251
257
  connection = self._connection(connection)
252
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
258
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
253
259
  parameters = parameters or []
254
260
 
255
261
  result = await connection.fetch(sql, parameters=parameters)
256
262
 
257
- if schema_type is None:
258
- result = cast("list[dict[str, Any]]", result.result()) # type: ignore[assignment]
259
- return cast("dict[str, Any]", result[0]) # type: ignore[index]
260
- return result.as_class(as_class=schema_type)[0]
263
+ # Convert to dict and use ResultConverter
264
+ dict_results = result.result()
265
+ if not dict_results:
266
+ self.check_not_found(None)
267
+
268
+ return self.to_schema(dict_results[0], schema_type=schema_type)
261
269
 
262
270
  @overload
263
271
  async def select_one_or_none(
264
272
  self,
265
273
  sql: str,
266
274
  parameters: "Optional[StatementParameterType]" = None,
267
- /,
268
- *,
275
+ *filters: "StatementFilter",
269
276
  connection: "Optional[PsqlpyConnection]" = None,
270
277
  schema_type: None = None,
271
278
  **kwargs: Any,
@@ -275,8 +282,7 @@ class PsqlpyDriver(
275
282
  self,
276
283
  sql: str,
277
284
  parameters: "Optional[StatementParameterType]" = None,
278
- /,
279
- *,
285
+ *filters: "StatementFilter",
280
286
  connection: "Optional[PsqlpyConnection]" = None,
281
287
  schema_type: "type[ModelDTOT]",
282
288
  **kwargs: Any,
@@ -284,35 +290,43 @@ class PsqlpyDriver(
284
290
  async def select_one_or_none(
285
291
  self,
286
292
  sql: str,
287
- parameters: Optional["StatementParameterType"] = None,
288
- /,
289
- *,
290
- connection: Optional["PsqlpyConnection"] = None,
293
+ parameters: "Optional[StatementParameterType]" = None,
294
+ *filters: "StatementFilter",
295
+ connection: "Optional[PsqlpyConnection]" = None,
291
296
  schema_type: "Optional[type[ModelDTOT]]" = None,
292
297
  **kwargs: Any,
293
298
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
299
+ """Fetch one row from the database or return None if no rows found.
300
+
301
+ Args:
302
+ sql: The SQL query string.
303
+ parameters: The parameters for the query (dict, tuple, list, or None).
304
+ *filters: Statement filters to apply.
305
+ connection: Optional connection override.
306
+ schema_type: Optional schema class for the result.
307
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
308
+
309
+ Returns:
310
+ The first row of the query results, or None if no results found.
311
+ """
294
312
  connection = self._connection(connection)
295
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
313
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
296
314
  parameters = parameters or []
297
315
 
298
316
  result = await connection.fetch(sql, parameters=parameters)
299
- if schema_type is None:
300
- result = cast("list[dict[str, Any]]", result.result()) # type: ignore[assignment]
301
- if len(result) == 0: # type: ignore[arg-type]
302
- return None
303
- return cast("dict[str, Any]", result[0]) # type: ignore[index]
304
- result = cast("list[ModelDTOT]", result.as_class(as_class=schema_type)) # type: ignore[assignment]
305
- if len(result) == 0: # type: ignore[arg-type]
317
+ dict_results = result.result()
318
+
319
+ if not dict_results:
306
320
  return None
307
- return cast("ModelDTOT", result[0]) # type: ignore[index]
321
+
322
+ return self.to_schema(dict_results[0], schema_type=schema_type)
308
323
 
309
324
  @overload
310
325
  async def select_value(
311
326
  self,
312
327
  sql: str,
313
328
  parameters: "Optional[StatementParameterType]" = None,
314
- /,
315
- *,
329
+ *filters: "StatementFilter",
316
330
  connection: "Optional[PsqlpyConnection]" = None,
317
331
  schema_type: None = None,
318
332
  **kwargs: Any,
@@ -322,8 +336,7 @@ class PsqlpyDriver(
322
336
  self,
323
337
  sql: str,
324
338
  parameters: "Optional[StatementParameterType]" = None,
325
- /,
326
- *,
339
+ *filters: "StatementFilter",
327
340
  connection: "Optional[PsqlpyConnection]" = None,
328
341
  schema_type: "type[T]",
329
342
  **kwargs: Any,
@@ -332,17 +345,30 @@ class PsqlpyDriver(
332
345
  self,
333
346
  sql: str,
334
347
  parameters: "Optional[StatementParameterType]" = None,
335
- /,
336
- *,
348
+ *filters: "StatementFilter",
337
349
  connection: "Optional[PsqlpyConnection]" = None,
338
350
  schema_type: "Optional[type[T]]" = None,
339
351
  **kwargs: Any,
340
352
  ) -> "Union[T, Any]":
353
+ """Fetch a single value from the database.
354
+
355
+ Args:
356
+ sql: The SQL query string.
357
+ parameters: The parameters for the query (dict, tuple, list, or None).
358
+ *filters: Statement filters to apply.
359
+ connection: Optional connection override.
360
+ schema_type: Optional type to convert the result to.
361
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
362
+
363
+ Returns:
364
+ The first value of the first row of the query results.
365
+ """
341
366
  connection = self._connection(connection)
342
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
367
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
343
368
  parameters = parameters or []
344
369
 
345
370
  value = await connection.fetch_val(sql, parameters=parameters)
371
+ value = self.check_not_found(value)
346
372
 
347
373
  if schema_type is None:
348
374
  return value
@@ -353,8 +379,7 @@ class PsqlpyDriver(
353
379
  self,
354
380
  sql: str,
355
381
  parameters: "Optional[StatementParameterType]" = None,
356
- /,
357
- *,
382
+ *filters: "StatementFilter",
358
383
  connection: "Optional[PsqlpyConnection]" = None,
359
384
  schema_type: None = None,
360
385
  **kwargs: Any,
@@ -364,8 +389,7 @@ class PsqlpyDriver(
364
389
  self,
365
390
  sql: str,
366
391
  parameters: "Optional[StatementParameterType]" = None,
367
- /,
368
- *,
392
+ *filters: "StatementFilter",
369
393
  connection: "Optional[PsqlpyConnection]" = None,
370
394
  schema_type: "type[T]",
371
395
  **kwargs: Any,
@@ -374,14 +398,26 @@ class PsqlpyDriver(
374
398
  self,
375
399
  sql: str,
376
400
  parameters: "Optional[StatementParameterType]" = None,
377
- /,
378
- *,
401
+ *filters: "StatementFilter",
379
402
  connection: "Optional[PsqlpyConnection]" = None,
380
403
  schema_type: "Optional[type[T]]" = None,
381
404
  **kwargs: Any,
382
405
  ) -> "Optional[Union[T, Any]]":
406
+ """Fetch a single value or None if not found.
407
+
408
+ Args:
409
+ sql: The SQL query string.
410
+ parameters: The parameters for the query (dict, tuple, list, or None).
411
+ *filters: Statement filters to apply.
412
+ connection: Optional connection override.
413
+ schema_type: Optional type to convert the result to.
414
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
415
+
416
+ Returns:
417
+ The first value of the first row of the query results, or None if no results found.
418
+ """
383
419
  connection = self._connection(connection)
384
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
420
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
385
421
  parameters = parameters or []
386
422
  try:
387
423
  value = await connection.fetch_val(sql, parameters=parameters)
@@ -397,14 +433,25 @@ class PsqlpyDriver(
397
433
  async def insert_update_delete(
398
434
  self,
399
435
  sql: str,
400
- parameters: Optional["StatementParameterType"] = None,
401
- /,
402
- *,
403
- connection: Optional["PsqlpyConnection"] = None,
436
+ parameters: "Optional[StatementParameterType]" = None,
437
+ *filters: "StatementFilter",
438
+ connection: "Optional[PsqlpyConnection]" = None,
404
439
  **kwargs: Any,
405
440
  ) -> int:
441
+ """Execute an insert, update, or delete statement.
442
+
443
+ Args:
444
+ sql: The SQL statement to execute.
445
+ parameters: The parameters for the statement (dict, tuple, list, or None).
446
+ *filters: Statement filters to apply.
447
+ connection: Optional connection override.
448
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
449
+
450
+ Returns:
451
+ The number of rows affected by the statement.
452
+ """
406
453
  connection = self._connection(connection)
407
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
454
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
408
455
  parameters = parameters or []
409
456
 
410
457
  await connection.execute(sql, parameters=parameters)
@@ -417,8 +464,7 @@ class PsqlpyDriver(
417
464
  self,
418
465
  sql: str,
419
466
  parameters: "Optional[StatementParameterType]" = None,
420
- /,
421
- *,
467
+ *filters: "StatementFilter",
422
468
  connection: "Optional[PsqlpyConnection]" = None,
423
469
  schema_type: None = None,
424
470
  **kwargs: Any,
@@ -428,8 +474,7 @@ class PsqlpyDriver(
428
474
  self,
429
475
  sql: str,
430
476
  parameters: "Optional[StatementParameterType]" = None,
431
- /,
432
- *,
477
+ *filters: "StatementFilter",
433
478
  connection: "Optional[PsqlpyConnection]" = None,
434
479
  schema_type: "type[ModelDTOT]",
435
480
  **kwargs: Any,
@@ -437,45 +482,63 @@ class PsqlpyDriver(
437
482
  async def insert_update_delete_returning(
438
483
  self,
439
484
  sql: str,
440
- parameters: Optional["StatementParameterType"] = None,
441
- /,
442
- *,
443
- connection: Optional["PsqlpyConnection"] = None,
485
+ parameters: "Optional[StatementParameterType]" = None,
486
+ *filters: "StatementFilter",
487
+ connection: "Optional[PsqlpyConnection]" = None,
444
488
  schema_type: "Optional[type[ModelDTOT]]" = None,
445
489
  **kwargs: Any,
446
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
490
+ ) -> "Union[ModelDTOT, dict[str, Any]]":
491
+ """Insert, update, or delete data with RETURNING clause.
492
+
493
+ Args:
494
+ sql: The SQL statement with RETURNING clause.
495
+ parameters: The parameters for the statement (dict, tuple, list, or None).
496
+ *filters: Statement filters to apply.
497
+ connection: Optional connection override.
498
+ schema_type: Optional schema class for the result.
499
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
500
+
501
+ Returns:
502
+ The returned row data, as either a model instance or dictionary.
503
+ """
447
504
  connection = self._connection(connection)
448
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
505
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
449
506
  parameters = parameters or []
450
507
 
451
- result = await connection.execute(sql, parameters=parameters)
452
- if schema_type is None:
453
- result = result.result() # type: ignore[assignment]
454
- if len(result) == 0: # type: ignore[arg-type]
455
- return None
456
- return cast("dict[str, Any]", result[0]) # type: ignore[index]
457
- result = result.as_class(as_class=schema_type) # type: ignore[assignment]
458
- if len(result) == 0: # type: ignore[arg-type]
459
- return None
460
- return cast("ModelDTOT", result[0]) # type: ignore[index]
508
+ result = await connection.fetch(sql, parameters=parameters)
509
+
510
+ dict_results = result.result()
511
+ if not dict_results:
512
+ self.check_not_found(None)
513
+
514
+ return self.to_schema(dict_results[0], schema_type=schema_type)
461
515
 
462
516
  async def execute_script(
463
517
  self,
464
518
  sql: str,
465
- parameters: Optional["StatementParameterType"] = None,
466
- /,
467
- *,
468
- connection: Optional["PsqlpyConnection"] = None,
519
+ parameters: "Optional[StatementParameterType]" = None,
520
+ connection: "Optional[PsqlpyConnection]" = None,
469
521
  **kwargs: Any,
470
522
  ) -> str:
523
+ """Execute a SQL script.
524
+
525
+ Args:
526
+ sql: The SQL script to execute.
527
+ parameters: The parameters for the script (dict, tuple, list, or None).
528
+ connection: Optional connection override.
529
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
530
+
531
+ Returns:
532
+ A success message.
533
+ """
471
534
  connection = self._connection(connection)
472
535
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
473
536
  parameters = parameters or []
474
537
 
475
538
  await connection.execute(sql, parameters=parameters)
476
- return sql
539
+ return "Script executed successfully"
477
540
 
478
- def _connection(self, connection: Optional["PsqlpyConnection"] = None) -> "PsqlpyConnection":
541
+ def _connection(self, connection: "Optional[PsqlpyConnection]" = None) -> "PsqlpyConnection":
479
542
  """Get the connection to use.
480
543
 
481
544
  Args: