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,17 +1,20 @@
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
20
  from collections.abc import Sequence
@@ -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
 
@@ -53,125 +65,96 @@ class PsqlpyDriver(
53
65
  sql: str,
54
66
  parameters: "Optional[StatementParameterType]" = None,
55
67
  /,
68
+ *filters: StatementFilter,
56
69
  **kwargs: Any,
57
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
70
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
58
71
  """Process SQL and parameters for psqlpy.
59
72
 
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
73
  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.
74
+ sql: SQL statement.
75
+ parameters: Query parameters.
76
+ *filters: Statement filters to apply.
77
+ **kwargs: Additional keyword arguments.
71
78
 
72
79
  Returns:
73
- A tuple of the processed SQL and parameters.
80
+ The SQL statement and parameters.
81
+
82
+ Raises:
83
+ SQLParsingError: If the SQL parsing fails.
74
84
  """
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
103
- param_index += 1
85
+ # Handle scalar parameter by converting to a single-item tuple
86
+ if parameters is not None and not isinstance(parameters, (list, tuple, dict)):
87
+ parameters = (parameters,)
104
88
 
105
- processed_sql_parts.append(sql[last_end:])
106
- final_sql = "".join(processed_sql_parts)
89
+ # Create and process the statement
90
+ statement = SQLStatement(sql=sql, parameters=parameters, kwargs=kwargs, dialect=self.dialect)
107
91
 
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,
113
- )
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
92
+ # Apply any filters
93
+ for filter_obj in filters:
94
+ statement = statement.apply_filter(filter_obj)
95
+
96
+ # Process the statement
97
+ sql, validated_params, parsed_expr = statement.process()
98
+
99
+ if validated_params is None:
100
+ return sql, None # psqlpy can handle None
101
+
102
+ # Convert positional parameters from question mark style to PostgreSQL's $N style
103
+ if isinstance(validated_params, (list, tuple)):
104
+ # Use a counter to generate $1, $2, etc. for each ? in the SQL that's outside strings/comments
105
+ param_index = 0
106
+
107
+ def replace_question_mark(match: Match[str]) -> str:
108
+ # Only process the match if it's not in a skipped context (string/comment)
109
+ if match.group(1): # This is a question mark outside string/comment
110
+ nonlocal param_index
141
111
  param_index += 1
112
+ return f"${param_index}"
113
+ # Return the entire matched text unchanged for strings/comments
114
+ return match.group(0)
115
+
116
+ return QUESTION_MARK_PATTERN.sub(replace_question_mark, sql), tuple(validated_params)
117
+
118
+ # If no parsed expression is available, we can't safely transform dictionary parameters
119
+ if is_dict(validated_params) and parsed_expr is None:
120
+ msg = f"psqlpy: SQL parsing failed and dictionary parameters were provided. Cannot determine parameter order without successful parse. SQL: {sql}"
121
+ raise SQLParsingError(msg)
122
+
123
+ # Convert dictionary parameters to the format expected by psqlpy
124
+ if is_dict(validated_params) and parsed_expr is not None:
125
+ # Find all named parameters in the SQL expression
126
+ named_params = []
127
+
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 validated_params:
130
+ named_params.append(node.name)
131
+ elif isinstance(node, exp.Placeholder) and isinstance(node.this, str) and node.this in validated_params:
132
+ named_params.append(node.this)
133
+
134
+ if named_params:
135
+ # Transform the SQL to use $1, $2, etc.
136
+ def convert_named_to_dollar(node: exp.Expression) -> exp.Expression:
137
+ if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
138
+ idx = named_params.index(node.name) + 1
139
+ return exp.Parameter(this=str(idx))
140
+ if (
141
+ isinstance(node, exp.Placeholder)
142
+ and isinstance(node.this, str)
143
+ and node.this in validated_params
144
+ ):
145
+ idx = named_params.index(node.this) + 1
146
+ return exp.Parameter(this=str(idx))
147
+ return node
148
+
149
+ return parsed_expr.transform(convert_named_to_dollar, copy=True).sql(dialect=self.dialect), tuple(
150
+ validated_params[name] for name in named_params
151
+ )
152
+
153
+ # If no named parameters were found in the SQL but dictionary was provided
154
+ return sql, tuple(validated_params.values())
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
+ # For any other case, return validated params
157
+ return sql, (validated_params,) if not isinstance(validated_params, (list, tuple)) else tuple(validated_params) # type: ignore[unreachable]
175
158
 
176
159
  # --- Public API Methods --- #
177
160
  @overload
@@ -180,7 +163,7 @@ class PsqlpyDriver(
180
163
  sql: str,
181
164
  parameters: "Optional[StatementParameterType]" = None,
182
165
  /,
183
- *,
166
+ *filters: StatementFilter,
184
167
  connection: "Optional[PsqlpyConnection]" = None,
185
168
  schema_type: None = None,
186
169
  **kwargs: Any,
@@ -191,7 +174,7 @@ class PsqlpyDriver(
191
174
  sql: str,
192
175
  parameters: "Optional[StatementParameterType]" = None,
193
176
  /,
194
- *,
177
+ *filters: StatementFilter,
195
178
  connection: "Optional[PsqlpyConnection]" = None,
196
179
  schema_type: "type[ModelDTOT]",
197
180
  **kwargs: Any,
@@ -199,22 +182,35 @@ class PsqlpyDriver(
199
182
  async def select(
200
183
  self,
201
184
  sql: str,
202
- parameters: Optional["StatementParameterType"] = None,
185
+ parameters: "Optional[StatementParameterType]" = None,
203
186
  /,
204
- *,
205
- connection: Optional["PsqlpyConnection"] = 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(
@@ -222,7 +218,7 @@ class PsqlpyDriver(
222
218
  sql: str,
223
219
  parameters: "Optional[StatementParameterType]" = None,
224
220
  /,
225
- *,
221
+ *filters: StatementFilter,
226
222
  connection: "Optional[PsqlpyConnection]" = None,
227
223
  schema_type: None = None,
228
224
  **kwargs: Any,
@@ -233,7 +229,7 @@ class PsqlpyDriver(
233
229
  sql: str,
234
230
  parameters: "Optional[StatementParameterType]" = None,
235
231
  /,
236
- *,
232
+ *filters: StatementFilter,
237
233
  connection: "Optional[PsqlpyConnection]" = None,
238
234
  schema_type: "type[ModelDTOT]",
239
235
  **kwargs: Any,
@@ -241,23 +237,38 @@ class PsqlpyDriver(
241
237
  async def select_one(
242
238
  self,
243
239
  sql: str,
244
- parameters: Optional["StatementParameterType"] = None,
240
+ parameters: "Optional[StatementParameterType]" = None,
245
241
  /,
246
- *,
247
- connection: Optional["PsqlpyConnection"] = None,
242
+ *filters: StatementFilter,
243
+ connection: "Optional[PsqlpyConnection]" = None,
248
244
  schema_type: "Optional[type[ModelDTOT]]" = None,
249
245
  **kwargs: Any,
250
246
  ) -> "Union[ModelDTOT, dict[str, Any]]":
247
+ """Fetch one row from the database.
248
+
249
+ Args:
250
+ sql: The SQL query string.
251
+ parameters: The parameters for the query (dict, tuple, list, or None).
252
+ *filters: Statement filters to apply.
253
+ connection: Optional connection override.
254
+ schema_type: Optional schema class for the result.
255
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
256
+
257
+ Returns:
258
+ The first row of the query results.
259
+ """
251
260
  connection = self._connection(connection)
252
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
261
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
253
262
  parameters = parameters or []
254
263
 
255
264
  result = await connection.fetch(sql, parameters=parameters)
256
265
 
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]
266
+ # Convert to dict and use ResultConverter
267
+ dict_results = result.result()
268
+ if not dict_results:
269
+ self.check_not_found(None)
270
+
271
+ return self.to_schema(dict_results[0], schema_type=schema_type)
261
272
 
262
273
  @overload
263
274
  async def select_one_or_none(
@@ -265,7 +276,7 @@ class PsqlpyDriver(
265
276
  sql: str,
266
277
  parameters: "Optional[StatementParameterType]" = None,
267
278
  /,
268
- *,
279
+ *filters: StatementFilter,
269
280
  connection: "Optional[PsqlpyConnection]" = None,
270
281
  schema_type: None = None,
271
282
  **kwargs: Any,
@@ -276,7 +287,7 @@ class PsqlpyDriver(
276
287
  sql: str,
277
288
  parameters: "Optional[StatementParameterType]" = None,
278
289
  /,
279
- *,
290
+ *filters: StatementFilter,
280
291
  connection: "Optional[PsqlpyConnection]" = None,
281
292
  schema_type: "type[ModelDTOT]",
282
293
  **kwargs: Any,
@@ -284,27 +295,37 @@ class PsqlpyDriver(
284
295
  async def select_one_or_none(
285
296
  self,
286
297
  sql: str,
287
- parameters: Optional["StatementParameterType"] = None,
298
+ parameters: "Optional[StatementParameterType]" = None,
288
299
  /,
289
- *,
290
- connection: Optional["PsqlpyConnection"] = None,
300
+ *filters: StatementFilter,
301
+ connection: "Optional[PsqlpyConnection]" = None,
291
302
  schema_type: "Optional[type[ModelDTOT]]" = None,
292
303
  **kwargs: Any,
293
304
  ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
305
+ """Fetch one row from the database or return None if no rows found.
306
+
307
+ Args:
308
+ sql: The SQL query string.
309
+ parameters: The parameters for the query (dict, tuple, list, or None).
310
+ *filters: Statement filters to apply.
311
+ connection: Optional connection override.
312
+ schema_type: Optional schema class for the result.
313
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
314
+
315
+ Returns:
316
+ The first row of the query results, or None if no results found.
317
+ """
294
318
  connection = self._connection(connection)
295
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
319
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
296
320
  parameters = parameters or []
297
321
 
298
322
  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]
323
+ dict_results = result.result()
324
+
325
+ if not dict_results:
306
326
  return None
307
- return cast("ModelDTOT", result[0]) # type: ignore[index]
327
+
328
+ return self.to_schema(dict_results[0], schema_type=schema_type)
308
329
 
309
330
  @overload
310
331
  async def select_value(
@@ -312,7 +333,7 @@ class PsqlpyDriver(
312
333
  sql: str,
313
334
  parameters: "Optional[StatementParameterType]" = None,
314
335
  /,
315
- *,
336
+ *filters: StatementFilter,
316
337
  connection: "Optional[PsqlpyConnection]" = None,
317
338
  schema_type: None = None,
318
339
  **kwargs: Any,
@@ -323,7 +344,7 @@ class PsqlpyDriver(
323
344
  sql: str,
324
345
  parameters: "Optional[StatementParameterType]" = None,
325
346
  /,
326
- *,
347
+ *filters: StatementFilter,
327
348
  connection: "Optional[PsqlpyConnection]" = None,
328
349
  schema_type: "type[T]",
329
350
  **kwargs: Any,
@@ -333,16 +354,30 @@ class PsqlpyDriver(
333
354
  sql: str,
334
355
  parameters: "Optional[StatementParameterType]" = None,
335
356
  /,
336
- *,
357
+ *filters: StatementFilter,
337
358
  connection: "Optional[PsqlpyConnection]" = None,
338
359
  schema_type: "Optional[type[T]]" = None,
339
360
  **kwargs: Any,
340
361
  ) -> "Union[T, Any]":
362
+ """Fetch a single value from the database.
363
+
364
+ Args:
365
+ sql: The SQL query string.
366
+ parameters: The parameters for the query (dict, tuple, list, or None).
367
+ *filters: Statement filters to apply.
368
+ connection: Optional connection override.
369
+ schema_type: Optional type to convert the result to.
370
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
371
+
372
+ Returns:
373
+ The first value of the first row of the query results.
374
+ """
341
375
  connection = self._connection(connection)
342
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
376
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
343
377
  parameters = parameters or []
344
378
 
345
379
  value = await connection.fetch_val(sql, parameters=parameters)
380
+ value = self.check_not_found(value)
346
381
 
347
382
  if schema_type is None:
348
383
  return value
@@ -354,7 +389,7 @@ class PsqlpyDriver(
354
389
  sql: str,
355
390
  parameters: "Optional[StatementParameterType]" = None,
356
391
  /,
357
- *,
392
+ *filters: StatementFilter,
358
393
  connection: "Optional[PsqlpyConnection]" = None,
359
394
  schema_type: None = None,
360
395
  **kwargs: Any,
@@ -365,7 +400,7 @@ class PsqlpyDriver(
365
400
  sql: str,
366
401
  parameters: "Optional[StatementParameterType]" = None,
367
402
  /,
368
- *,
403
+ *filters: StatementFilter,
369
404
  connection: "Optional[PsqlpyConnection]" = None,
370
405
  schema_type: "type[T]",
371
406
  **kwargs: Any,
@@ -375,13 +410,26 @@ class PsqlpyDriver(
375
410
  sql: str,
376
411
  parameters: "Optional[StatementParameterType]" = None,
377
412
  /,
378
- *,
413
+ *filters: StatementFilter,
379
414
  connection: "Optional[PsqlpyConnection]" = None,
380
415
  schema_type: "Optional[type[T]]" = None,
381
416
  **kwargs: Any,
382
417
  ) -> "Optional[Union[T, Any]]":
418
+ """Fetch a single value or None if not found.
419
+
420
+ Args:
421
+ sql: The SQL query string.
422
+ parameters: The parameters for the query (dict, tuple, list, or None).
423
+ *filters: Statement filters to apply.
424
+ connection: Optional connection override.
425
+ schema_type: Optional type to convert the result to.
426
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
427
+
428
+ Returns:
429
+ The first value of the first row of the query results, or None if no results found.
430
+ """
383
431
  connection = self._connection(connection)
384
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
432
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
385
433
  parameters = parameters or []
386
434
  try:
387
435
  value = await connection.fetch_val(sql, parameters=parameters)
@@ -397,14 +445,26 @@ class PsqlpyDriver(
397
445
  async def insert_update_delete(
398
446
  self,
399
447
  sql: str,
400
- parameters: Optional["StatementParameterType"] = None,
448
+ parameters: "Optional[StatementParameterType]" = None,
401
449
  /,
402
- *,
403
- connection: Optional["PsqlpyConnection"] = None,
450
+ *filters: StatementFilter,
451
+ connection: "Optional[PsqlpyConnection]" = None,
404
452
  **kwargs: Any,
405
453
  ) -> int:
454
+ """Execute an insert, update, or delete statement.
455
+
456
+ Args:
457
+ sql: The SQL statement to execute.
458
+ parameters: The parameters for the statement (dict, tuple, list, or None).
459
+ *filters: Statement filters to apply.
460
+ connection: Optional connection override.
461
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
462
+
463
+ Returns:
464
+ The number of rows affected by the statement.
465
+ """
406
466
  connection = self._connection(connection)
407
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
467
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
408
468
  parameters = parameters or []
409
469
 
410
470
  await connection.execute(sql, parameters=parameters)
@@ -418,7 +478,7 @@ class PsqlpyDriver(
418
478
  sql: str,
419
479
  parameters: "Optional[StatementParameterType]" = None,
420
480
  /,
421
- *,
481
+ *filters: StatementFilter,
422
482
  connection: "Optional[PsqlpyConnection]" = None,
423
483
  schema_type: None = None,
424
484
  **kwargs: Any,
@@ -429,7 +489,7 @@ class PsqlpyDriver(
429
489
  sql: str,
430
490
  parameters: "Optional[StatementParameterType]" = None,
431
491
  /,
432
- *,
492
+ *filters: StatementFilter,
433
493
  connection: "Optional[PsqlpyConnection]" = None,
434
494
  schema_type: "type[ModelDTOT]",
435
495
  **kwargs: Any,
@@ -437,45 +497,65 @@ class PsqlpyDriver(
437
497
  async def insert_update_delete_returning(
438
498
  self,
439
499
  sql: str,
440
- parameters: Optional["StatementParameterType"] = None,
500
+ parameters: "Optional[StatementParameterType]" = None,
441
501
  /,
442
- *,
443
- connection: Optional["PsqlpyConnection"] = None,
502
+ *filters: StatementFilter,
503
+ connection: "Optional[PsqlpyConnection]" = None,
444
504
  schema_type: "Optional[type[ModelDTOT]]" = None,
445
505
  **kwargs: Any,
446
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
506
+ ) -> "Union[ModelDTOT, dict[str, Any]]":
507
+ """Insert, update, or delete data from the database and return result.
508
+
509
+ Args:
510
+ sql: The SQL statement to execute.
511
+ parameters: The parameters for the statement (dict, tuple, list, or None).
512
+ *filters: Statement filters to apply.
513
+ connection: Optional connection override.
514
+ schema_type: Optional schema class for the result.
515
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
516
+
517
+ Returns:
518
+ The first row of results.
519
+ """
447
520
  connection = self._connection(connection)
448
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
521
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
449
522
  parameters = parameters or []
450
523
 
451
524
  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]
525
+ dict_results = result.result()
526
+
527
+ if not dict_results:
528
+ self.check_not_found(None)
529
+
530
+ return self.to_schema(dict_results[0], schema_type=schema_type)
461
531
 
462
532
  async def execute_script(
463
533
  self,
464
534
  sql: str,
465
- parameters: Optional["StatementParameterType"] = None,
535
+ parameters: "Optional[StatementParameterType]" = None,
466
536
  /,
467
- *,
468
- connection: Optional["PsqlpyConnection"] = None,
537
+ connection: "Optional[PsqlpyConnection]" = None,
469
538
  **kwargs: Any,
470
539
  ) -> str:
540
+ """Execute a SQL script.
541
+
542
+ Args:
543
+ sql: The SQL script to execute.
544
+ parameters: The parameters for the script (dict, tuple, list, or None).
545
+ connection: Optional connection override.
546
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
547
+
548
+ Returns:
549
+ A success message.
550
+ """
471
551
  connection = self._connection(connection)
472
552
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
473
553
  parameters = parameters or []
474
554
 
475
555
  await connection.execute(sql, parameters=parameters)
476
- return sql
556
+ return "Script executed successfully"
477
557
 
478
- def _connection(self, connection: Optional["PsqlpyConnection"] = None) -> "PsqlpyConnection":
558
+ def _connection(self, connection: "Optional[PsqlpyConnection]" = None) -> "PsqlpyConnection":
479
559
  """Get the connection to use.
480
560
 
481
561
  Args: