sqlspec 0.8.0__py3-none-any.whl → 0.9.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.
- sqlspec/_typing.py +39 -6
- sqlspec/adapters/adbc/__init__.py +2 -2
- sqlspec/adapters/adbc/config.py +34 -11
- sqlspec/adapters/adbc/driver.py +167 -108
- sqlspec/adapters/aiosqlite/__init__.py +2 -2
- sqlspec/adapters/aiosqlite/config.py +2 -2
- sqlspec/adapters/aiosqlite/driver.py +28 -39
- sqlspec/adapters/asyncmy/__init__.py +3 -3
- sqlspec/adapters/asyncmy/config.py +11 -12
- sqlspec/adapters/asyncmy/driver.py +25 -34
- sqlspec/adapters/asyncpg/__init__.py +5 -5
- sqlspec/adapters/asyncpg/config.py +17 -19
- sqlspec/adapters/asyncpg/driver.py +249 -93
- sqlspec/adapters/duckdb/__init__.py +2 -2
- sqlspec/adapters/duckdb/config.py +2 -2
- sqlspec/adapters/duckdb/driver.py +49 -49
- sqlspec/adapters/oracledb/__init__.py +8 -8
- sqlspec/adapters/oracledb/config/__init__.py +6 -6
- sqlspec/adapters/oracledb/config/_asyncio.py +9 -10
- sqlspec/adapters/oracledb/config/_sync.py +8 -9
- sqlspec/adapters/oracledb/driver.py +114 -41
- sqlspec/adapters/psqlpy/__init__.py +0 -0
- sqlspec/adapters/psqlpy/config.py +258 -0
- sqlspec/adapters/psqlpy/driver.py +335 -0
- sqlspec/adapters/psycopg/__init__.py +10 -5
- sqlspec/adapters/psycopg/config/__init__.py +6 -6
- sqlspec/adapters/psycopg/config/_async.py +12 -12
- sqlspec/adapters/psycopg/config/_sync.py +13 -13
- sqlspec/adapters/psycopg/driver.py +180 -218
- sqlspec/adapters/sqlite/__init__.py +2 -2
- sqlspec/adapters/sqlite/config.py +2 -2
- sqlspec/adapters/sqlite/driver.py +43 -41
- sqlspec/base.py +275 -153
- sqlspec/exceptions.py +30 -0
- sqlspec/extensions/litestar/config.py +6 -0
- sqlspec/extensions/litestar/handlers.py +25 -0
- sqlspec/extensions/litestar/plugin.py +6 -1
- sqlspec/statement.py +373 -0
- sqlspec/typing.py +10 -1
- {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/METADATA +4 -1
- sqlspec-0.9.0.dist-info/RECORD +61 -0
- sqlspec-0.8.0.dist-info/RECORD +0 -57
- {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.8.0.dist-info → sqlspec-0.9.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from contextlib import asynccontextmanager, contextmanager
|
|
2
3
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
3
4
|
|
|
4
5
|
from psycopg.rows import dict_row
|
|
5
6
|
|
|
6
|
-
from sqlspec.base import
|
|
7
|
+
from sqlspec.base import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol, T
|
|
8
|
+
from sqlspec.exceptions import SQLParsingError
|
|
9
|
+
from sqlspec.statement import PARAM_REGEX, SQLStatement
|
|
7
10
|
|
|
8
11
|
if TYPE_CHECKING:
|
|
9
12
|
from collections.abc import AsyncGenerator, Generator
|
|
@@ -12,6 +15,8 @@ if TYPE_CHECKING:
|
|
|
12
15
|
|
|
13
16
|
from sqlspec.typing import ModelDTOT, StatementParameterType
|
|
14
17
|
|
|
18
|
+
logger = logging.getLogger("sqlspec")
|
|
19
|
+
|
|
15
20
|
__all__ = ("PsycopgAsyncDriver", "PsycopgSyncDriver")
|
|
16
21
|
|
|
17
22
|
|
|
@@ -19,11 +24,63 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
19
24
|
"""Psycopg Sync Driver Adapter."""
|
|
20
25
|
|
|
21
26
|
connection: "Connection"
|
|
22
|
-
|
|
27
|
+
dialect: str = "postgres"
|
|
23
28
|
|
|
24
29
|
def __init__(self, connection: "Connection") -> None:
|
|
25
30
|
self.connection = connection
|
|
26
31
|
|
|
32
|
+
def _process_sql_params(
|
|
33
|
+
self,
|
|
34
|
+
sql: str,
|
|
35
|
+
parameters: "Optional[StatementParameterType]" = None,
|
|
36
|
+
/,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
|
|
39
|
+
"""Process SQL and parameters, converting :name -> %(name)s if needed."""
|
|
40
|
+
stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
|
|
41
|
+
processed_sql, processed_params = stmt.process()
|
|
42
|
+
|
|
43
|
+
if isinstance(processed_params, dict):
|
|
44
|
+
parameter_dict = processed_params
|
|
45
|
+
processed_sql_parts: list[str] = []
|
|
46
|
+
last_end = 0
|
|
47
|
+
found_params_regex: list[str] = []
|
|
48
|
+
|
|
49
|
+
for match in PARAM_REGEX.finditer(processed_sql):
|
|
50
|
+
if match.group("dquote") or match.group("squote") or match.group("comment"):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if match.group("var_name"):
|
|
54
|
+
var_name = match.group("var_name")
|
|
55
|
+
found_params_regex.append(var_name)
|
|
56
|
+
start = match.start("var_name") - 1
|
|
57
|
+
end = match.end("var_name")
|
|
58
|
+
|
|
59
|
+
if var_name not in parameter_dict:
|
|
60
|
+
msg = (
|
|
61
|
+
f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
|
|
62
|
+
f"Processed SQL: {processed_sql}"
|
|
63
|
+
)
|
|
64
|
+
raise SQLParsingError(msg)
|
|
65
|
+
|
|
66
|
+
processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
|
|
67
|
+
last_end = end
|
|
68
|
+
|
|
69
|
+
processed_sql_parts.append(processed_sql[last_end:])
|
|
70
|
+
final_sql = "".join(processed_sql_parts)
|
|
71
|
+
|
|
72
|
+
if not found_params_regex and parameter_dict:
|
|
73
|
+
logger.warning(
|
|
74
|
+
"Dict params provided (%s), but no :name placeholders found. SQL: %s",
|
|
75
|
+
list(parameter_dict.keys()),
|
|
76
|
+
processed_sql,
|
|
77
|
+
)
|
|
78
|
+
return processed_sql, parameter_dict
|
|
79
|
+
|
|
80
|
+
return final_sql, parameter_dict
|
|
81
|
+
|
|
82
|
+
return processed_sql, processed_params
|
|
83
|
+
|
|
27
84
|
@staticmethod
|
|
28
85
|
@contextmanager
|
|
29
86
|
def _with_cursor(connection: "Connection") -> "Generator[Any, None, None]":
|
|
@@ -33,75 +90,15 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
33
90
|
finally:
|
|
34
91
|
cursor.close()
|
|
35
92
|
|
|
36
|
-
def _process_sql_params(
|
|
37
|
-
self, sql: str, parameters: "Optional[StatementParameterType]" = None
|
|
38
|
-
) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
|
|
39
|
-
"""Process SQL query and parameters for DB-API execution.
|
|
40
|
-
|
|
41
|
-
Converts named parameters (:name) to positional parameters (%s)
|
|
42
|
-
if the input parameters are a dictionary.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
sql: The SQL query string.
|
|
46
|
-
parameters: The parameters for the query (dict, tuple, list, or None).
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
A tuple containing the processed SQL string and the processed parameters
|
|
50
|
-
(always a tuple or None if the input was a dictionary, otherwise the original type).
|
|
51
|
-
|
|
52
|
-
Raises:
|
|
53
|
-
ValueError: If a named parameter in the SQL is not found in the dictionary
|
|
54
|
-
or if a parameter in the dictionary is not used in the SQL.
|
|
55
|
-
"""
|
|
56
|
-
if not isinstance(parameters, dict) or not parameters:
|
|
57
|
-
# If parameters are not a dict, or empty dict, assume positional/no params
|
|
58
|
-
# Let the underlying driver handle tuples/lists directly
|
|
59
|
-
return sql, parameters
|
|
60
|
-
|
|
61
|
-
processed_sql = ""
|
|
62
|
-
processed_params_list: list[Any] = []
|
|
63
|
-
last_end = 0
|
|
64
|
-
found_params: set[str] = set()
|
|
65
|
-
|
|
66
|
-
for match in PARAM_REGEX.finditer(sql):
|
|
67
|
-
if match.group("dquote") is not None or match.group("squote") is not None:
|
|
68
|
-
# Skip placeholders within quotes
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
var_name = match.group("var_name")
|
|
72
|
-
if var_name is None: # Should not happen with the regex, but safeguard
|
|
73
|
-
continue
|
|
74
|
-
|
|
75
|
-
if var_name not in parameters:
|
|
76
|
-
msg = f"Named parameter ':{var_name}' found in SQL but not provided in parameters dictionary."
|
|
77
|
-
raise ValueError(msg)
|
|
78
|
-
|
|
79
|
-
# Append segment before the placeholder + the driver's positional placeholder
|
|
80
|
-
processed_sql += sql[last_end : match.start("var_name") - 1] + "%s"
|
|
81
|
-
processed_params_list.append(parameters[var_name])
|
|
82
|
-
found_params.add(var_name)
|
|
83
|
-
last_end = match.end("var_name")
|
|
84
|
-
|
|
85
|
-
# Append the rest of the SQL string
|
|
86
|
-
processed_sql += sql[last_end:]
|
|
87
|
-
|
|
88
|
-
# Check if all provided parameters were used
|
|
89
|
-
unused_params = set(parameters.keys()) - found_params
|
|
90
|
-
if unused_params:
|
|
91
|
-
msg = f"Parameters provided but not found in SQL: {unused_params}"
|
|
92
|
-
# Depending on desired strictness, this could be a warning or an error
|
|
93
|
-
# For now, let's raise an error for clarity
|
|
94
|
-
raise ValueError(msg)
|
|
95
|
-
|
|
96
|
-
return processed_sql, tuple(processed_params_list)
|
|
97
|
-
|
|
98
93
|
def select(
|
|
99
94
|
self,
|
|
100
95
|
sql: str,
|
|
101
96
|
parameters: "Optional[StatementParameterType]" = None,
|
|
102
97
|
/,
|
|
103
|
-
|
|
98
|
+
*,
|
|
104
99
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
100
|
+
connection: "Optional[Connection]" = None,
|
|
101
|
+
**kwargs: Any,
|
|
105
102
|
) -> "list[Union[ModelDTOT, dict[str, Any]]]":
|
|
106
103
|
"""Fetch data from the database.
|
|
107
104
|
|
|
@@ -109,7 +106,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
109
106
|
List of row data as either model instances or dictionaries.
|
|
110
107
|
"""
|
|
111
108
|
connection = self._connection(connection)
|
|
112
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
109
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
113
110
|
with self._with_cursor(connection) as cursor:
|
|
114
111
|
cursor.execute(sql, parameters)
|
|
115
112
|
results = cursor.fetchall()
|
|
@@ -125,8 +122,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
125
122
|
sql: str,
|
|
126
123
|
parameters: "Optional[StatementParameterType]" = None,
|
|
127
124
|
/,
|
|
125
|
+
*,
|
|
128
126
|
connection: "Optional[Connection]" = None,
|
|
129
127
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
128
|
+
**kwargs: Any,
|
|
130
129
|
) -> "Union[ModelDTOT, dict[str, Any]]":
|
|
131
130
|
"""Fetch one row from the database.
|
|
132
131
|
|
|
@@ -134,8 +133,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
134
133
|
The first row of the query results.
|
|
135
134
|
"""
|
|
136
135
|
connection = self._connection(connection)
|
|
137
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
138
|
-
|
|
136
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
139
137
|
with self._with_cursor(connection) as cursor:
|
|
140
138
|
cursor.execute(sql, parameters)
|
|
141
139
|
row = cursor.fetchone()
|
|
@@ -149,8 +147,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
149
147
|
sql: str,
|
|
150
148
|
parameters: "Optional[StatementParameterType]" = None,
|
|
151
149
|
/,
|
|
150
|
+
*,
|
|
152
151
|
connection: "Optional[Connection]" = None,
|
|
153
152
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
154
|
) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
|
|
155
155
|
"""Fetch one row from the database.
|
|
156
156
|
|
|
@@ -158,8 +158,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
158
158
|
The first row of the query results.
|
|
159
159
|
"""
|
|
160
160
|
connection = self._connection(connection)
|
|
161
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
162
|
-
|
|
161
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
163
162
|
with self._with_cursor(connection) as cursor:
|
|
164
163
|
cursor.execute(sql, parameters)
|
|
165
164
|
row = cursor.fetchone()
|
|
@@ -174,8 +173,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
174
173
|
sql: str,
|
|
175
174
|
parameters: "Optional[StatementParameterType]" = None,
|
|
176
175
|
/,
|
|
176
|
+
*,
|
|
177
177
|
connection: "Optional[Connection]" = None,
|
|
178
178
|
schema_type: "Optional[type[T]]" = None,
|
|
179
|
+
**kwargs: Any,
|
|
179
180
|
) -> "Union[T, Any]":
|
|
180
181
|
"""Fetch a single value from the database.
|
|
181
182
|
|
|
@@ -183,13 +184,13 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
183
184
|
The first value from the first row of results, or None if no results.
|
|
184
185
|
"""
|
|
185
186
|
connection = self._connection(connection)
|
|
186
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
187
|
-
|
|
187
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
188
188
|
with self._with_cursor(connection) as cursor:
|
|
189
189
|
cursor.execute(sql, parameters)
|
|
190
190
|
row = cursor.fetchone()
|
|
191
191
|
row = self.check_not_found(row)
|
|
192
|
-
val = next(iter(row))
|
|
192
|
+
val = next(iter(row.values())) if row else None
|
|
193
|
+
val = self.check_not_found(val)
|
|
193
194
|
if schema_type is not None:
|
|
194
195
|
return schema_type(val) # type: ignore[call-arg]
|
|
195
196
|
return val
|
|
@@ -199,8 +200,10 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
199
200
|
sql: str,
|
|
200
201
|
parameters: "Optional[StatementParameterType]" = None,
|
|
201
202
|
/,
|
|
203
|
+
*,
|
|
202
204
|
connection: "Optional[Connection]" = None,
|
|
203
205
|
schema_type: "Optional[type[T]]" = None,
|
|
206
|
+
**kwargs: Any,
|
|
204
207
|
) -> "Optional[Union[T, Any]]":
|
|
205
208
|
"""Fetch a single value from the database.
|
|
206
209
|
|
|
@@ -208,14 +211,15 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
208
211
|
The first value from the first row of results, or None if no results.
|
|
209
212
|
"""
|
|
210
213
|
connection = self._connection(connection)
|
|
211
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
212
|
-
|
|
214
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
213
215
|
with self._with_cursor(connection) as cursor:
|
|
214
216
|
cursor.execute(sql, parameters)
|
|
215
217
|
row = cursor.fetchone()
|
|
216
218
|
if row is None:
|
|
217
219
|
return None
|
|
218
|
-
val = next(iter(row))
|
|
220
|
+
val = next(iter(row.values())) if row else None
|
|
221
|
+
if val is None:
|
|
222
|
+
return None
|
|
219
223
|
if schema_type is not None:
|
|
220
224
|
return schema_type(val) # type: ignore[call-arg]
|
|
221
225
|
return val
|
|
@@ -225,27 +229,30 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
225
229
|
sql: str,
|
|
226
230
|
parameters: "Optional[StatementParameterType]" = None,
|
|
227
231
|
/,
|
|
232
|
+
*,
|
|
228
233
|
connection: "Optional[Connection]" = None,
|
|
234
|
+
**kwargs: Any,
|
|
229
235
|
) -> int:
|
|
230
|
-
"""
|
|
236
|
+
"""Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
|
|
231
237
|
|
|
232
238
|
Returns:
|
|
233
|
-
|
|
239
|
+
The number of rows affected by the operation.
|
|
234
240
|
"""
|
|
235
241
|
connection = self._connection(connection)
|
|
236
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
237
|
-
|
|
242
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
238
243
|
with self._with_cursor(connection) as cursor:
|
|
239
244
|
cursor.execute(sql, parameters)
|
|
240
|
-
return
|
|
245
|
+
return getattr(cursor, "rowcount", -1) # pyright: ignore[reportUnknownMemberType]
|
|
241
246
|
|
|
242
247
|
def insert_update_delete_returning(
|
|
243
248
|
self,
|
|
244
249
|
sql: str,
|
|
245
250
|
parameters: "Optional[StatementParameterType]" = None,
|
|
246
251
|
/,
|
|
252
|
+
*,
|
|
247
253
|
connection: "Optional[Connection]" = None,
|
|
248
254
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
255
|
+
**kwargs: Any,
|
|
249
256
|
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
250
257
|
"""Insert, update, or delete data from the database and return result.
|
|
251
258
|
|
|
@@ -253,8 +260,7 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
253
260
|
The first row of results.
|
|
254
261
|
"""
|
|
255
262
|
connection = self._connection(connection)
|
|
256
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
257
|
-
|
|
263
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
258
264
|
with self._with_cursor(connection) as cursor:
|
|
259
265
|
cursor.execute(sql, parameters)
|
|
260
266
|
result = cursor.fetchone()
|
|
@@ -271,7 +277,9 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
271
277
|
sql: str,
|
|
272
278
|
parameters: "Optional[StatementParameterType]" = None,
|
|
273
279
|
/,
|
|
280
|
+
*,
|
|
274
281
|
connection: "Optional[Connection]" = None,
|
|
282
|
+
**kwargs: Any,
|
|
275
283
|
) -> str:
|
|
276
284
|
"""Execute a script.
|
|
277
285
|
|
|
@@ -279,49 +287,73 @@ class PsycopgSyncDriver(SyncDriverAdapterProtocol["Connection"]):
|
|
|
279
287
|
Status message for the operation.
|
|
280
288
|
"""
|
|
281
289
|
connection = self._connection(connection)
|
|
282
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
283
|
-
|
|
284
|
-
with self._with_cursor(connection) as cursor:
|
|
285
|
-
cursor.execute(sql, parameters)
|
|
286
|
-
return str(cursor.rowcount)
|
|
287
|
-
|
|
288
|
-
def execute_script_returning(
|
|
289
|
-
self,
|
|
290
|
-
sql: str,
|
|
291
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
292
|
-
/,
|
|
293
|
-
connection: "Optional[Connection]" = None,
|
|
294
|
-
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
295
|
-
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
296
|
-
"""Execute a script and return result.
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
The first row of results.
|
|
300
|
-
"""
|
|
301
|
-
connection = self._connection(connection)
|
|
302
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
303
|
-
|
|
290
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
304
291
|
with self._with_cursor(connection) as cursor:
|
|
305
292
|
cursor.execute(sql, parameters)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if result is None:
|
|
309
|
-
return None
|
|
310
|
-
|
|
311
|
-
if schema_type is not None:
|
|
312
|
-
return cast("ModelDTOT", schema_type(**result)) # pyright: ignore[reportUnknownArgumentType]
|
|
313
|
-
return cast("dict[str, Any]", result) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
|
|
293
|
+
return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"
|
|
314
294
|
|
|
315
295
|
|
|
316
296
|
class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
317
297
|
"""Psycopg Async Driver Adapter."""
|
|
318
298
|
|
|
319
299
|
connection: "AsyncConnection"
|
|
320
|
-
|
|
300
|
+
dialect: str = "postgres"
|
|
321
301
|
|
|
322
302
|
def __init__(self, connection: "AsyncConnection") -> None:
|
|
323
303
|
self.connection = connection
|
|
324
304
|
|
|
305
|
+
def _process_sql_params(
|
|
306
|
+
self,
|
|
307
|
+
sql: str,
|
|
308
|
+
parameters: "Optional[StatementParameterType]" = None,
|
|
309
|
+
/,
|
|
310
|
+
**kwargs: Any,
|
|
311
|
+
) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
|
|
312
|
+
"""Process SQL and parameters, converting :name -> %(name)s if needed."""
|
|
313
|
+
stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
|
|
314
|
+
processed_sql, processed_params = stmt.process()
|
|
315
|
+
|
|
316
|
+
if isinstance(processed_params, dict):
|
|
317
|
+
parameter_dict = processed_params
|
|
318
|
+
processed_sql_parts: list[str] = []
|
|
319
|
+
last_end = 0
|
|
320
|
+
found_params_regex: list[str] = []
|
|
321
|
+
|
|
322
|
+
for match in PARAM_REGEX.finditer(processed_sql):
|
|
323
|
+
if match.group("dquote") or match.group("squote") or match.group("comment"):
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
if match.group("var_name"):
|
|
327
|
+
var_name = match.group("var_name")
|
|
328
|
+
found_params_regex.append(var_name)
|
|
329
|
+
start = match.start("var_name") - 1
|
|
330
|
+
end = match.end("var_name")
|
|
331
|
+
|
|
332
|
+
if var_name not in parameter_dict:
|
|
333
|
+
msg = (
|
|
334
|
+
f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
|
|
335
|
+
f"Processed SQL: {processed_sql}"
|
|
336
|
+
)
|
|
337
|
+
raise SQLParsingError(msg)
|
|
338
|
+
|
|
339
|
+
processed_sql_parts.extend((processed_sql[last_end:start], f"%({var_name})s"))
|
|
340
|
+
last_end = end
|
|
341
|
+
|
|
342
|
+
processed_sql_parts.append(processed_sql[last_end:])
|
|
343
|
+
final_sql = "".join(processed_sql_parts)
|
|
344
|
+
|
|
345
|
+
if not found_params_regex and parameter_dict:
|
|
346
|
+
logger.warning(
|
|
347
|
+
"Dict params provided (%s), but no :name placeholders found. SQL: %s",
|
|
348
|
+
list(parameter_dict.keys()),
|
|
349
|
+
processed_sql,
|
|
350
|
+
)
|
|
351
|
+
return processed_sql, parameter_dict
|
|
352
|
+
|
|
353
|
+
return final_sql, parameter_dict
|
|
354
|
+
|
|
355
|
+
return processed_sql, processed_params
|
|
356
|
+
|
|
325
357
|
@staticmethod
|
|
326
358
|
@asynccontextmanager
|
|
327
359
|
async def _with_cursor(connection: "AsyncConnection") -> "AsyncGenerator[Any, None]":
|
|
@@ -331,75 +363,15 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
331
363
|
finally:
|
|
332
364
|
await cursor.close()
|
|
333
365
|
|
|
334
|
-
def _process_sql_params(
|
|
335
|
-
self, sql: str, parameters: "Optional[StatementParameterType]" = None
|
|
336
|
-
) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
|
|
337
|
-
"""Process SQL query and parameters for DB-API execution.
|
|
338
|
-
|
|
339
|
-
Converts named parameters (:name) to positional parameters (%s)
|
|
340
|
-
if the input parameters are a dictionary.
|
|
341
|
-
|
|
342
|
-
Args:
|
|
343
|
-
sql: The SQL query string.
|
|
344
|
-
parameters: The parameters for the query (dict, tuple, list, or None).
|
|
345
|
-
|
|
346
|
-
Returns:
|
|
347
|
-
A tuple containing the processed SQL string and the processed parameters
|
|
348
|
-
(always a tuple or None if the input was a dictionary, otherwise the original type).
|
|
349
|
-
|
|
350
|
-
Raises:
|
|
351
|
-
ValueError: If a named parameter in the SQL is not found in the dictionary
|
|
352
|
-
or if a parameter in the dictionary is not used in the SQL.
|
|
353
|
-
"""
|
|
354
|
-
if not isinstance(parameters, dict) or not parameters:
|
|
355
|
-
# If parameters are not a dict, or empty dict, assume positional/no params
|
|
356
|
-
# Let the underlying driver handle tuples/lists directly
|
|
357
|
-
return sql, parameters
|
|
358
|
-
|
|
359
|
-
processed_sql = ""
|
|
360
|
-
processed_params_list: list[Any] = []
|
|
361
|
-
last_end = 0
|
|
362
|
-
found_params: set[str] = set()
|
|
363
|
-
|
|
364
|
-
for match in PARAM_REGEX.finditer(sql):
|
|
365
|
-
if match.group("dquote") is not None or match.group("squote") is not None:
|
|
366
|
-
# Skip placeholders within quotes
|
|
367
|
-
continue
|
|
368
|
-
|
|
369
|
-
var_name = match.group("var_name")
|
|
370
|
-
if var_name is None: # Should not happen with the regex, but safeguard
|
|
371
|
-
continue
|
|
372
|
-
|
|
373
|
-
if var_name not in parameters:
|
|
374
|
-
msg = f"Named parameter ':{var_name}' found in SQL but not provided in parameters dictionary."
|
|
375
|
-
raise ValueError(msg)
|
|
376
|
-
|
|
377
|
-
# Append segment before the placeholder + the driver's positional placeholder
|
|
378
|
-
processed_sql += sql[last_end : match.start("var_name") - 1] + "%s"
|
|
379
|
-
processed_params_list.append(parameters[var_name])
|
|
380
|
-
found_params.add(var_name)
|
|
381
|
-
last_end = match.end("var_name")
|
|
382
|
-
|
|
383
|
-
# Append the rest of the SQL string
|
|
384
|
-
processed_sql += sql[last_end:]
|
|
385
|
-
|
|
386
|
-
# Check if all provided parameters were used
|
|
387
|
-
unused_params = set(parameters.keys()) - found_params
|
|
388
|
-
if unused_params:
|
|
389
|
-
msg = f"Parameters provided but not found in SQL: {unused_params}"
|
|
390
|
-
# Depending on desired strictness, this could be a warning or an error
|
|
391
|
-
# For now, let's raise an error for clarity
|
|
392
|
-
raise ValueError(msg)
|
|
393
|
-
|
|
394
|
-
return processed_sql, tuple(processed_params_list)
|
|
395
|
-
|
|
396
366
|
async def select(
|
|
397
367
|
self,
|
|
398
368
|
sql: str,
|
|
399
369
|
parameters: "Optional[StatementParameterType]" = None,
|
|
400
370
|
/,
|
|
371
|
+
*,
|
|
401
372
|
connection: "Optional[AsyncConnection]" = None,
|
|
402
373
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
374
|
+
**kwargs: Any,
|
|
403
375
|
) -> "list[Union[ModelDTOT, dict[str, Any]]]":
|
|
404
376
|
"""Fetch data from the database.
|
|
405
377
|
|
|
@@ -407,7 +379,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
407
379
|
List of row data as either model instances or dictionaries.
|
|
408
380
|
"""
|
|
409
381
|
connection = self._connection(connection)
|
|
410
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
382
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
411
383
|
results: list[Union[ModelDTOT, dict[str, Any]]] = []
|
|
412
384
|
|
|
413
385
|
async with self._with_cursor(connection) as cursor:
|
|
@@ -424,8 +396,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
424
396
|
sql: str,
|
|
425
397
|
parameters: "Optional[StatementParameterType]" = None,
|
|
426
398
|
/,
|
|
399
|
+
*,
|
|
427
400
|
connection: "Optional[AsyncConnection]" = None,
|
|
428
401
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
402
|
+
**kwargs: Any,
|
|
429
403
|
) -> "Union[ModelDTOT, dict[str, Any]]":
|
|
430
404
|
"""Fetch one row from the database.
|
|
431
405
|
|
|
@@ -433,7 +407,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
433
407
|
The first row of the query results.
|
|
434
408
|
"""
|
|
435
409
|
connection = self._connection(connection)
|
|
436
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
410
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
437
411
|
|
|
438
412
|
async with self._with_cursor(connection) as cursor:
|
|
439
413
|
await cursor.execute(sql, parameters)
|
|
@@ -448,8 +422,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
448
422
|
sql: str,
|
|
449
423
|
parameters: "Optional[StatementParameterType]" = None,
|
|
450
424
|
/,
|
|
451
|
-
|
|
425
|
+
*,
|
|
452
426
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
427
|
+
connection: "Optional[AsyncConnection]" = None,
|
|
428
|
+
**kwargs: Any,
|
|
453
429
|
) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
|
|
454
430
|
"""Fetch one row from the database.
|
|
455
431
|
|
|
@@ -457,7 +433,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
457
433
|
The first row of the query results.
|
|
458
434
|
"""
|
|
459
435
|
connection = self._connection(connection)
|
|
460
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
436
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
461
437
|
|
|
462
438
|
async with self._with_cursor(connection) as cursor:
|
|
463
439
|
await cursor.execute(sql, parameters)
|
|
@@ -473,22 +449,25 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
473
449
|
sql: str,
|
|
474
450
|
parameters: "Optional[StatementParameterType]" = None,
|
|
475
451
|
/,
|
|
452
|
+
*,
|
|
476
453
|
connection: "Optional[AsyncConnection]" = None,
|
|
477
454
|
schema_type: "Optional[type[T]]" = None,
|
|
478
|
-
|
|
455
|
+
**kwargs: Any,
|
|
456
|
+
) -> "Union[T, Any]":
|
|
479
457
|
"""Fetch a single value from the database.
|
|
480
458
|
|
|
481
459
|
Returns:
|
|
482
460
|
The first value from the first row of results, or None if no results.
|
|
483
461
|
"""
|
|
484
462
|
connection = self._connection(connection)
|
|
485
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
463
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
486
464
|
|
|
487
465
|
async with self._with_cursor(connection) as cursor:
|
|
488
466
|
await cursor.execute(sql, parameters)
|
|
489
467
|
row = await cursor.fetchone()
|
|
490
468
|
row = self.check_not_found(row)
|
|
491
|
-
val = next(iter(row))
|
|
469
|
+
val = next(iter(row.values())) if row else None
|
|
470
|
+
val = self.check_not_found(val)
|
|
492
471
|
if schema_type is not None:
|
|
493
472
|
return schema_type(val) # type: ignore[call-arg]
|
|
494
473
|
return val
|
|
@@ -498,8 +477,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
498
477
|
sql: str,
|
|
499
478
|
parameters: "Optional[StatementParameterType]" = None,
|
|
500
479
|
/,
|
|
480
|
+
*,
|
|
501
481
|
connection: "Optional[AsyncConnection]" = None,
|
|
502
482
|
schema_type: "Optional[type[T]]" = None,
|
|
483
|
+
**kwargs: Any,
|
|
503
484
|
) -> "Optional[Union[T, Any]]":
|
|
504
485
|
"""Fetch a single value from the database.
|
|
505
486
|
|
|
@@ -507,14 +488,16 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
507
488
|
The first value from the first row of results, or None if no results.
|
|
508
489
|
"""
|
|
509
490
|
connection = self._connection(connection)
|
|
510
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
491
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
511
492
|
|
|
512
493
|
async with self._with_cursor(connection) as cursor:
|
|
513
494
|
await cursor.execute(sql, parameters)
|
|
514
495
|
row = await cursor.fetchone()
|
|
515
496
|
if row is None:
|
|
516
497
|
return None
|
|
517
|
-
val = next(iter(row))
|
|
498
|
+
val = next(iter(row.values())) if row else None
|
|
499
|
+
if val is None:
|
|
500
|
+
return None
|
|
518
501
|
if schema_type is not None:
|
|
519
502
|
return schema_type(val) # type: ignore[call-arg]
|
|
520
503
|
return val
|
|
@@ -524,15 +507,17 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
524
507
|
sql: str,
|
|
525
508
|
parameters: "Optional[StatementParameterType]" = None,
|
|
526
509
|
/,
|
|
510
|
+
*,
|
|
527
511
|
connection: "Optional[AsyncConnection]" = None,
|
|
512
|
+
**kwargs: Any,
|
|
528
513
|
) -> int:
|
|
529
|
-
"""
|
|
514
|
+
"""Execute an INSERT, UPDATE, or DELETE query and return the number of affected rows.
|
|
530
515
|
|
|
531
516
|
Returns:
|
|
532
|
-
|
|
517
|
+
The number of rows affected by the operation.
|
|
533
518
|
"""
|
|
534
519
|
connection = self._connection(connection)
|
|
535
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
520
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
536
521
|
|
|
537
522
|
async with self._with_cursor(connection) as cursor:
|
|
538
523
|
await cursor.execute(sql, parameters)
|
|
@@ -547,8 +532,10 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
547
532
|
sql: str,
|
|
548
533
|
parameters: "Optional[StatementParameterType]" = None,
|
|
549
534
|
/,
|
|
535
|
+
*,
|
|
550
536
|
connection: "Optional[AsyncConnection]" = None,
|
|
551
537
|
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
538
|
+
**kwargs: Any,
|
|
552
539
|
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
553
540
|
"""Insert, update, or delete data from the database and return result.
|
|
554
541
|
|
|
@@ -556,7 +543,7 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
556
543
|
The first row of results.
|
|
557
544
|
"""
|
|
558
545
|
connection = self._connection(connection)
|
|
559
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
546
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
560
547
|
|
|
561
548
|
async with self._with_cursor(connection) as cursor:
|
|
562
549
|
await cursor.execute(sql, parameters)
|
|
@@ -574,7 +561,9 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
574
561
|
sql: str,
|
|
575
562
|
parameters: "Optional[StatementParameterType]" = None,
|
|
576
563
|
/,
|
|
564
|
+
*,
|
|
577
565
|
connection: "Optional[AsyncConnection]" = None,
|
|
566
|
+
**kwargs: Any,
|
|
578
567
|
) -> str:
|
|
579
568
|
"""Execute a script.
|
|
580
569
|
|
|
@@ -582,35 +571,8 @@ class PsycopgAsyncDriver(AsyncDriverAdapterProtocol["AsyncConnection"]):
|
|
|
582
571
|
Status message for the operation.
|
|
583
572
|
"""
|
|
584
573
|
connection = self._connection(connection)
|
|
585
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
574
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
586
575
|
|
|
587
576
|
async with self._with_cursor(connection) as cursor:
|
|
588
577
|
await cursor.execute(sql, parameters)
|
|
589
|
-
return str(cursor.
|
|
590
|
-
|
|
591
|
-
async def execute_script_returning(
|
|
592
|
-
self,
|
|
593
|
-
sql: str,
|
|
594
|
-
parameters: "Optional[StatementParameterType]" = None,
|
|
595
|
-
/,
|
|
596
|
-
connection: "Optional[AsyncConnection]" = None,
|
|
597
|
-
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
598
|
-
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
599
|
-
"""Execute a script and return result.
|
|
600
|
-
|
|
601
|
-
Returns:
|
|
602
|
-
The first row of results.
|
|
603
|
-
"""
|
|
604
|
-
connection = self._connection(connection)
|
|
605
|
-
sql, parameters = self._process_sql_params(sql, parameters)
|
|
606
|
-
|
|
607
|
-
async with self._with_cursor(connection) as cursor:
|
|
608
|
-
await cursor.execute(sql, parameters)
|
|
609
|
-
result = await cursor.fetchone()
|
|
610
|
-
|
|
611
|
-
if result is None:
|
|
612
|
-
return None
|
|
613
|
-
|
|
614
|
-
if schema_type is not None:
|
|
615
|
-
return cast("ModelDTOT", schema_type(**result)) # pyright: ignore[reportUnknownArgumentType]
|
|
616
|
-
return cast("dict[str, Any]", result) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
|
|
578
|
+
return str(cursor.statusmessage) if cursor.statusmessage is not None else "DONE"
|