sqlspec 0.7.1__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/__init__.py +15 -0
- sqlspec/_serialization.py +16 -2
- sqlspec/_typing.py +40 -7
- sqlspec/adapters/adbc/__init__.py +7 -0
- sqlspec/adapters/adbc/config.py +183 -17
- sqlspec/adapters/adbc/driver.py +392 -0
- sqlspec/adapters/aiosqlite/__init__.py +5 -1
- sqlspec/adapters/aiosqlite/config.py +24 -6
- sqlspec/adapters/aiosqlite/driver.py +264 -0
- sqlspec/adapters/asyncmy/__init__.py +7 -2
- sqlspec/adapters/asyncmy/config.py +71 -11
- sqlspec/adapters/asyncmy/driver.py +246 -0
- sqlspec/adapters/asyncpg/__init__.py +9 -0
- sqlspec/adapters/asyncpg/config.py +102 -25
- sqlspec/adapters/asyncpg/driver.py +444 -0
- sqlspec/adapters/duckdb/__init__.py +5 -1
- sqlspec/adapters/duckdb/config.py +194 -12
- sqlspec/adapters/duckdb/driver.py +225 -0
- sqlspec/adapters/oracledb/__init__.py +7 -4
- sqlspec/adapters/oracledb/config/__init__.py +4 -4
- sqlspec/adapters/oracledb/config/_asyncio.py +96 -12
- sqlspec/adapters/oracledb/config/_common.py +1 -1
- sqlspec/adapters/oracledb/config/_sync.py +96 -12
- sqlspec/adapters/oracledb/driver.py +571 -0
- 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 +16 -0
- sqlspec/adapters/psycopg/config/__init__.py +6 -6
- sqlspec/adapters/psycopg/config/_async.py +107 -15
- sqlspec/adapters/psycopg/config/_common.py +2 -2
- sqlspec/adapters/psycopg/config/_sync.py +107 -15
- sqlspec/adapters/psycopg/driver.py +578 -0
- sqlspec/adapters/sqlite/__init__.py +7 -0
- sqlspec/adapters/sqlite/config.py +24 -6
- sqlspec/adapters/sqlite/driver.py +305 -0
- sqlspec/base.py +565 -63
- sqlspec/exceptions.py +30 -0
- sqlspec/extensions/litestar/__init__.py +19 -0
- sqlspec/extensions/litestar/_utils.py +56 -0
- sqlspec/extensions/litestar/config.py +87 -0
- sqlspec/extensions/litestar/handlers.py +213 -0
- sqlspec/extensions/litestar/plugin.py +105 -11
- sqlspec/statement.py +373 -0
- sqlspec/typing.py +81 -17
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/fixtures.py +4 -5
- sqlspec/utils/sync_tools.py +335 -0
- {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/METADATA +4 -1
- sqlspec-0.9.0.dist-info/RECORD +61 -0
- sqlspec-0.7.1.dist-info/RECORD +0 -46
- {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
|
-
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
4
4
|
|
|
5
5
|
from asyncpg import Record
|
|
6
6
|
from asyncpg import create_pool as asyncpg_create_pool
|
|
7
|
-
from asyncpg.pool import
|
|
8
|
-
from typing_extensions import TypeAlias
|
|
7
|
+
from asyncpg.pool import PoolConnectionProxy
|
|
9
8
|
|
|
10
9
|
from sqlspec._serialization import decode_json, encode_json
|
|
10
|
+
from sqlspec.adapters.asyncpg.driver import AsyncpgConnection, AsyncpgDriver
|
|
11
11
|
from sqlspec.base import AsyncDatabaseConfig, GenericPoolConfig
|
|
12
12
|
from sqlspec.exceptions import ImproperConfigurationError
|
|
13
13
|
from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
|
|
@@ -17,21 +17,20 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine
|
|
18
18
|
|
|
19
19
|
from asyncpg.connection import Connection
|
|
20
|
+
from asyncpg.pool import Pool
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
__all__ = (
|
|
23
|
-
"
|
|
24
|
-
"
|
|
24
|
+
"AsyncpgConfig",
|
|
25
|
+
"AsyncpgPoolConfig",
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
T = TypeVar("T")
|
|
29
30
|
|
|
30
|
-
PgConnection: TypeAlias = "Union[Connection, PoolConnectionProxy]" # pyright: ignore[reportMissingTypeArgument]
|
|
31
|
-
|
|
32
31
|
|
|
33
32
|
@dataclass
|
|
34
|
-
class
|
|
33
|
+
class AsyncpgPoolConfig(GenericPoolConfig):
|
|
35
34
|
"""Configuration for Asyncpg's :class:`Pool <asyncpg.pool.Pool>`.
|
|
36
35
|
|
|
37
36
|
For details see: https://magicstack.github.io/asyncpg/current/api/index.html#connection-pools
|
|
@@ -52,7 +51,7 @@ class AsyncPgPoolConfig(GenericPoolConfig):
|
|
|
52
51
|
min_size: "Union[int, EmptyType]" = Empty
|
|
53
52
|
"""The number of connections to keep open inside the connection pool."""
|
|
54
53
|
max_size: "Union[int, EmptyType]" = Empty
|
|
55
|
-
"""The number of connections to allow in connection pool
|
|
54
|
+
"""The number of connections to allow in connection pool "overflow", that is connections that can be opened above
|
|
56
55
|
and beyond the pool_size setting, which defaults to 10."""
|
|
57
56
|
|
|
58
57
|
max_queries: "Union[int, EmptyType]" = Empty
|
|
@@ -71,23 +70,55 @@ class AsyncPgPoolConfig(GenericPoolConfig):
|
|
|
71
70
|
|
|
72
71
|
|
|
73
72
|
@dataclass
|
|
74
|
-
class
|
|
73
|
+
class AsyncpgConfig(AsyncDatabaseConfig["AsyncpgConnection", "Pool", "AsyncpgDriver"]): # pyright: ignore[reportMissingTypeArgument]
|
|
75
74
|
"""Asyncpg Configuration."""
|
|
76
75
|
|
|
77
|
-
pool_config: "Optional[
|
|
76
|
+
pool_config: "Optional[AsyncpgPoolConfig]" = field(default=None)
|
|
78
77
|
"""Asyncpg Pool configuration"""
|
|
79
|
-
json_deserializer: "Callable[[str], Any]" = decode_json
|
|
78
|
+
json_deserializer: "Callable[[str], Any]" = field(hash=False, default=decode_json)
|
|
80
79
|
"""For dialects that support the :class:`JSON <sqlalchemy.types.JSON>` datatype, this is a Python callable that will
|
|
81
80
|
convert a JSON string to a Python object. By default, this is set to SQLSpec's
|
|
82
81
|
:attr:`decode_json() <sqlspec._serialization.decode_json>` function."""
|
|
83
|
-
json_serializer: "Callable[[Any], str]" = encode_json
|
|
82
|
+
json_serializer: "Callable[[Any], str]" = field(hash=False, default=encode_json)
|
|
84
83
|
"""For dialects that support the JSON datatype, this is a Python callable that will render a given object as JSON.
|
|
85
84
|
By default, SQLSpec's :attr:`encode_json() <sqlspec._serialization.encode_json>` is used."""
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
connection_type: "type[AsyncpgConnection]" = field(
|
|
86
|
+
hash=False, init=False, default_factory=lambda: PoolConnectionProxy
|
|
87
|
+
)
|
|
88
|
+
"""Type of the connection object"""
|
|
89
|
+
driver_type: "type[AsyncpgDriver]" = field(hash=False, init=False, default_factory=lambda: AsyncpgDriver) # type: ignore[type-abstract,unused-ignore]
|
|
90
|
+
"""Type of the driver object"""
|
|
91
|
+
pool_instance: "Optional[Pool[Any]]" = field(hash=False, default=None)
|
|
92
|
+
"""The connection pool instance. If set, this will be used instead of creating a new pool."""
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
""
|
|
94
|
+
@property
|
|
95
|
+
def connection_config_dict(self) -> "dict[str, Any]":
|
|
96
|
+
"""Return the connection configuration as a dict.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A string keyed dict of config kwargs for the asyncpg.connect function.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ImproperConfigurationError: If the connection configuration is not provided.
|
|
103
|
+
"""
|
|
104
|
+
if self.pool_config:
|
|
105
|
+
connect_dict: dict[str, Any] = {}
|
|
106
|
+
|
|
107
|
+
# Add dsn if available
|
|
108
|
+
if hasattr(self.pool_config, "dsn"):
|
|
109
|
+
connect_dict["dsn"] = self.pool_config.dsn
|
|
110
|
+
|
|
111
|
+
# Add any connect_kwargs if available
|
|
112
|
+
if (
|
|
113
|
+
hasattr(self.pool_config, "connect_kwargs")
|
|
114
|
+
and self.pool_config.connect_kwargs is not Empty
|
|
115
|
+
and isinstance(self.pool_config.connect_kwargs, dict)
|
|
116
|
+
):
|
|
117
|
+
connect_dict.update(dict(self.pool_config.connect_kwargs.items()))
|
|
118
|
+
|
|
119
|
+
return connect_dict
|
|
120
|
+
msg = "You must provide a 'pool_config' for this adapter."
|
|
121
|
+
raise ImproperConfigurationError(msg)
|
|
91
122
|
|
|
92
123
|
@property
|
|
93
124
|
def pool_config_dict(self) -> "dict[str, Any]":
|
|
@@ -96,9 +127,17 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
|
|
|
96
127
|
Returns:
|
|
97
128
|
A string keyed dict of config kwargs for the Asyncpg :func:`create_pool <asyncpg.pool.create_pool>`
|
|
98
129
|
function.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ImproperConfigurationError: If no pool_config is provided but a pool_instance is set.
|
|
99
133
|
"""
|
|
100
134
|
if self.pool_config:
|
|
101
|
-
return dataclass_to_dict(
|
|
135
|
+
return dataclass_to_dict(
|
|
136
|
+
self.pool_config,
|
|
137
|
+
exclude_empty=True,
|
|
138
|
+
exclude={"pool_instance", "driver_type", "connection_type"},
|
|
139
|
+
convert_nested=False,
|
|
140
|
+
)
|
|
102
141
|
msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
|
|
103
142
|
raise ImproperConfigurationError(msg)
|
|
104
143
|
|
|
@@ -107,6 +146,10 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
|
|
|
107
146
|
|
|
108
147
|
Returns:
|
|
109
148
|
Getter that returns the pool instance used by the plugin.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ImproperConfigurationError: If neither pool_config nor pool_instance are provided,
|
|
152
|
+
or if the pool could not be configured.
|
|
110
153
|
"""
|
|
111
154
|
if self.pool_instance is not None:
|
|
112
155
|
return self.pool_instance
|
|
@@ -117,11 +160,9 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
|
|
|
117
160
|
|
|
118
161
|
pool_config = self.pool_config_dict
|
|
119
162
|
self.pool_instance = await asyncpg_create_pool(**pool_config)
|
|
120
|
-
if self.pool_instance is None:
|
|
121
|
-
msg = "Could not configure the 'pool_instance'. Please check your configuration."
|
|
122
|
-
raise ImproperConfigurationError(
|
|
123
|
-
msg,
|
|
124
|
-
)
|
|
163
|
+
if self.pool_instance is None: # pyright: ignore[reportUnnecessaryComparison]
|
|
164
|
+
msg = "Could not configure the 'pool_instance'. Please check your configuration." # type: ignore[unreachable]
|
|
165
|
+
raise ImproperConfigurationError(msg)
|
|
125
166
|
return self.pool_instance
|
|
126
167
|
|
|
127
168
|
def provide_pool(self, *args: "Any", **kwargs: "Any") -> "Awaitable[Pool]": # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType]
|
|
@@ -132,13 +173,49 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
|
|
|
132
173
|
"""
|
|
133
174
|
return self.create_pool() # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
|
|
134
175
|
|
|
176
|
+
async def create_connection(self) -> "AsyncpgConnection":
|
|
177
|
+
"""Create and return a new asyncpg connection from the pool.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A Connection instance.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ImproperConfigurationError: If the connection could not be created.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
pool = await self.provide_pool()
|
|
187
|
+
return await pool.acquire()
|
|
188
|
+
except Exception as e:
|
|
189
|
+
msg = f"Could not configure the asyncpg connection. Error: {e!s}"
|
|
190
|
+
raise ImproperConfigurationError(msg) from e
|
|
191
|
+
|
|
135
192
|
@asynccontextmanager
|
|
136
|
-
async def provide_connection(
|
|
193
|
+
async def provide_connection(
|
|
194
|
+
self, *args: "Any", **kwargs: "Any"
|
|
195
|
+
) -> "AsyncGenerator[PoolConnectionProxy[Any], None]": # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType]
|
|
137
196
|
"""Create a connection instance.
|
|
138
197
|
|
|
139
|
-
|
|
198
|
+
Yields:
|
|
140
199
|
A connection instance.
|
|
141
200
|
"""
|
|
142
201
|
db_pool = await self.provide_pool(*args, **kwargs) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
|
|
143
202
|
async with db_pool.acquire() as connection: # pyright: ignore[reportUnknownVariableType]
|
|
144
203
|
yield connection
|
|
204
|
+
|
|
205
|
+
async def close_pool(self) -> None:
|
|
206
|
+
"""Close the pool."""
|
|
207
|
+
if self.pool_instance is not None:
|
|
208
|
+
await self.pool_instance.close()
|
|
209
|
+
self.pool_instance = None
|
|
210
|
+
|
|
211
|
+
@asynccontextmanager
|
|
212
|
+
async def provide_session(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[AsyncpgDriver, None]":
|
|
213
|
+
"""Create and provide a database session.
|
|
214
|
+
|
|
215
|
+
Yields:
|
|
216
|
+
A Aiosqlite driver instance.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
async with self.provide_connection(*args, **kwargs) as connection:
|
|
221
|
+
yield self.driver_type(connection)
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from asyncpg import Connection
|
|
6
|
+
from typing_extensions import TypeAlias
|
|
7
|
+
|
|
8
|
+
from sqlspec.base import AsyncDriverAdapterProtocol, T
|
|
9
|
+
from sqlspec.exceptions import SQLParsingError
|
|
10
|
+
from sqlspec.statement import PARAM_REGEX, SQLStatement
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from asyncpg.connection import Connection
|
|
14
|
+
from asyncpg.pool import PoolConnectionProxy
|
|
15
|
+
|
|
16
|
+
from sqlspec.typing import ModelDTOT, StatementParameterType
|
|
17
|
+
|
|
18
|
+
__all__ = ("AsyncpgConnection", "AsyncpgDriver")
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("sqlspec")
|
|
21
|
+
|
|
22
|
+
# Regex to find '?' placeholders, skipping those inside quotes or SQL comments
|
|
23
|
+
# Simplified version, assumes standard SQL quoting/comments
|
|
24
|
+
QMARK_REGEX = re.compile(
|
|
25
|
+
r"""(?P<dquote>"[^"]*") | # Double-quoted strings
|
|
26
|
+
(?P<squote>\'[^\']*\') | # Single-quoted strings
|
|
27
|
+
(?P<comment>--[^\n]*|/\*.*?\*/) | # SQL comments (single/multi-line)
|
|
28
|
+
(?P<qmark>\?) # The question mark placeholder
|
|
29
|
+
""",
|
|
30
|
+
re.VERBOSE | re.DOTALL,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
AsyncpgConnection: TypeAlias = "Union[Connection[Any], PoolConnectionProxy[Any]]" # pyright: ignore[reportMissingTypeArgument]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncpgDriver(AsyncDriverAdapterProtocol["AsyncpgConnection"]):
|
|
37
|
+
"""AsyncPG Postgres Driver Adapter."""
|
|
38
|
+
|
|
39
|
+
connection: "AsyncpgConnection"
|
|
40
|
+
dialect: str = "postgres"
|
|
41
|
+
|
|
42
|
+
def __init__(self, connection: "AsyncpgConnection") -> None:
|
|
43
|
+
self.connection = connection
|
|
44
|
+
|
|
45
|
+
def _process_sql_params( # noqa: C901, PLR0912, PLR0915
|
|
46
|
+
self,
|
|
47
|
+
sql: str,
|
|
48
|
+
parameters: "Optional[StatementParameterType]" = None,
|
|
49
|
+
/,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
|
|
52
|
+
# Use SQLStatement for parameter validation and merging first
|
|
53
|
+
# It also handles potential dialect-specific logic if implemented there.
|
|
54
|
+
stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
|
|
55
|
+
sql, parameters = stmt.process()
|
|
56
|
+
|
|
57
|
+
# Case 1: Parameters are effectively a dictionary (either passed as dict or via kwargs merged by SQLStatement)
|
|
58
|
+
if isinstance(parameters, dict):
|
|
59
|
+
processed_sql_parts: list[str] = []
|
|
60
|
+
ordered_params = []
|
|
61
|
+
last_end = 0
|
|
62
|
+
param_index = 1
|
|
63
|
+
found_params_regex: list[str] = []
|
|
64
|
+
|
|
65
|
+
# Manually parse the PROCESSED SQL for :name -> $n conversion
|
|
66
|
+
for match in PARAM_REGEX.finditer(sql):
|
|
67
|
+
# Skip matches inside quotes or comments
|
|
68
|
+
if match.group("dquote") or match.group("squote") or match.group("comment"):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if match.group("var_name"): # Finds :var_name
|
|
72
|
+
var_name = match.group("var_name")
|
|
73
|
+
found_params_regex.append(var_name)
|
|
74
|
+
start = match.start("var_name") - 1 # Include the ':'
|
|
75
|
+
end = match.end("var_name")
|
|
76
|
+
|
|
77
|
+
# SQLStatement should have already validated parameter existence,
|
|
78
|
+
# but we double-check here during ordering.
|
|
79
|
+
if var_name not in parameters:
|
|
80
|
+
# This should ideally not happen if SQLStatement validation is robust.
|
|
81
|
+
msg = (
|
|
82
|
+
f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
|
|
83
|
+
f"Processed SQL: {sql}"
|
|
84
|
+
)
|
|
85
|
+
raise SQLParsingError(msg)
|
|
86
|
+
|
|
87
|
+
processed_sql_parts.extend((sql[last_end:start], f"${param_index}"))
|
|
88
|
+
ordered_params.append(parameters[var_name])
|
|
89
|
+
last_end = end
|
|
90
|
+
param_index += 1
|
|
91
|
+
|
|
92
|
+
processed_sql_parts.append(sql[last_end:])
|
|
93
|
+
final_sql = "".join(processed_sql_parts)
|
|
94
|
+
|
|
95
|
+
# --- Validation ---
|
|
96
|
+
# Check if named placeholders were found if dict params were provided
|
|
97
|
+
# SQLStatement might handle this validation, but a warning here can be useful.
|
|
98
|
+
if not found_params_regex and parameters:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Dict params provided (%s), but no :name placeholders found. SQL: %s",
|
|
101
|
+
list(parameters.keys()),
|
|
102
|
+
sql,
|
|
103
|
+
)
|
|
104
|
+
# If no placeholders, return original SQL from SQLStatement and empty tuple for asyncpg
|
|
105
|
+
return sql, ()
|
|
106
|
+
|
|
107
|
+
# Additional checks (potentially redundant if SQLStatement covers them):
|
|
108
|
+
# 1. Ensure all found placeholders have corresponding params (covered by check inside loop)
|
|
109
|
+
# 2. Ensure all provided params correspond to a placeholder
|
|
110
|
+
provided_keys = set(parameters.keys())
|
|
111
|
+
found_keys = set(found_params_regex)
|
|
112
|
+
unused_keys = provided_keys - found_keys
|
|
113
|
+
if unused_keys:
|
|
114
|
+
# SQLStatement might handle this, but log a warning just in case.
|
|
115
|
+
logger.warning(
|
|
116
|
+
"Parameters provided but not used in SQL: %s. SQL: %s",
|
|
117
|
+
unused_keys,
|
|
118
|
+
sql,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return final_sql, tuple(ordered_params) # asyncpg expects a sequence
|
|
122
|
+
|
|
123
|
+
# Case 2: Parameters are effectively a sequence/scalar (merged by SQLStatement)
|
|
124
|
+
if isinstance(parameters, (list, tuple)):
|
|
125
|
+
# Parameters are a sequence, need to convert ? -> $n
|
|
126
|
+
sequence_processed_parts: list[str] = []
|
|
127
|
+
param_index = 1
|
|
128
|
+
last_end = 0
|
|
129
|
+
qmark_found = False
|
|
130
|
+
|
|
131
|
+
# Manually parse the PROCESSED SQL to find '?' outside comments/quotes and convert to $n
|
|
132
|
+
for match in QMARK_REGEX.finditer(sql):
|
|
133
|
+
if match.group("dquote") or match.group("squote") or match.group("comment"):
|
|
134
|
+
continue # Skip quotes and comments
|
|
135
|
+
|
|
136
|
+
if match.group("qmark"):
|
|
137
|
+
qmark_found = True
|
|
138
|
+
start = match.start("qmark")
|
|
139
|
+
end = match.end("qmark")
|
|
140
|
+
sequence_processed_parts.extend((sql[last_end:start], f"${param_index}"))
|
|
141
|
+
last_end = end
|
|
142
|
+
param_index += 1
|
|
143
|
+
|
|
144
|
+
sequence_processed_parts.append(sql[last_end:])
|
|
145
|
+
final_sql = "".join(sequence_processed_parts)
|
|
146
|
+
|
|
147
|
+
# --- Validation ---
|
|
148
|
+
# Check if '?' was found if parameters were provided
|
|
149
|
+
if parameters and not qmark_found:
|
|
150
|
+
# SQLStatement might allow this, log a warning.
|
|
151
|
+
logger.warning(
|
|
152
|
+
"Sequence/scalar parameters provided, but no '?' placeholders found. SQL: %s",
|
|
153
|
+
sql,
|
|
154
|
+
)
|
|
155
|
+
# Return PROCESSED SQL from SQLStatement as no conversion happened here
|
|
156
|
+
return sql, parameters
|
|
157
|
+
|
|
158
|
+
# Check parameter count match (using count from manual parsing vs count from stmt)
|
|
159
|
+
expected_params = param_index - 1
|
|
160
|
+
actual_params = len(parameters)
|
|
161
|
+
if expected_params != actual_params:
|
|
162
|
+
msg = (
|
|
163
|
+
f"Parameter count mismatch: Processed SQL expected {expected_params} parameters ('$n'), "
|
|
164
|
+
f"but {actual_params} were provided by SQLStatement. "
|
|
165
|
+
f"Final Processed SQL: {final_sql}"
|
|
166
|
+
)
|
|
167
|
+
raise SQLParsingError(msg)
|
|
168
|
+
|
|
169
|
+
return final_sql, parameters
|
|
170
|
+
|
|
171
|
+
# Case 3: Parameters are None (as determined by SQLStatement)
|
|
172
|
+
# processed_params is None
|
|
173
|
+
# Check if the SQL contains any placeholders unexpectedly
|
|
174
|
+
# Check for :name style
|
|
175
|
+
named_placeholders_found = False
|
|
176
|
+
for match in PARAM_REGEX.finditer(sql):
|
|
177
|
+
if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group(
|
|
178
|
+
"var_name"
|
|
179
|
+
):
|
|
180
|
+
named_placeholders_found = True
|
|
181
|
+
break
|
|
182
|
+
if named_placeholders_found:
|
|
183
|
+
msg = f"Processed SQL contains named parameters (:name) but no parameters were provided. SQL: {sql}"
|
|
184
|
+
raise SQLParsingError(msg)
|
|
185
|
+
|
|
186
|
+
# Check for ? style
|
|
187
|
+
qmark_placeholders_found = False
|
|
188
|
+
for match in QMARK_REGEX.finditer(sql):
|
|
189
|
+
if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group("qmark"):
|
|
190
|
+
qmark_placeholders_found = True
|
|
191
|
+
break
|
|
192
|
+
if qmark_placeholders_found:
|
|
193
|
+
msg = f"Processed SQL contains positional parameters (?) but no parameters were provided. SQL: {sql}"
|
|
194
|
+
raise SQLParsingError(msg)
|
|
195
|
+
|
|
196
|
+
# No parameters provided and none found in SQL, return original SQL from SQLStatement and empty tuple
|
|
197
|
+
return sql, () # asyncpg expects a sequence, even if empty
|
|
198
|
+
|
|
199
|
+
async def select(
|
|
200
|
+
self,
|
|
201
|
+
sql: str,
|
|
202
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
203
|
+
/,
|
|
204
|
+
*,
|
|
205
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
206
|
+
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
207
|
+
**kwargs: Any,
|
|
208
|
+
) -> "list[Union[ModelDTOT, dict[str, Any]]]":
|
|
209
|
+
"""Fetch data from the database.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
sql: SQL statement.
|
|
213
|
+
parameters: Query parameters.
|
|
214
|
+
connection: Optional connection to use.
|
|
215
|
+
schema_type: Optional schema class for the result.
|
|
216
|
+
**kwargs: Additional keyword arguments.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of row data as either model instances or dictionaries.
|
|
220
|
+
"""
|
|
221
|
+
connection = self._connection(connection)
|
|
222
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
223
|
+
parameters = parameters if parameters is not None else {}
|
|
224
|
+
|
|
225
|
+
results = await connection.fetch(sql, *parameters) # pyright: ignore
|
|
226
|
+
if not results:
|
|
227
|
+
return []
|
|
228
|
+
if schema_type is None:
|
|
229
|
+
return [dict(row.items()) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
230
|
+
return [cast("ModelDTOT", schema_type(**dict(row.items()))) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
231
|
+
|
|
232
|
+
async def select_one(
|
|
233
|
+
self,
|
|
234
|
+
sql: str,
|
|
235
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
236
|
+
/,
|
|
237
|
+
*,
|
|
238
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
239
|
+
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
240
|
+
**kwargs: Any,
|
|
241
|
+
) -> "Union[ModelDTOT, dict[str, Any]]":
|
|
242
|
+
"""Fetch one row from the database.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
sql: SQL statement.
|
|
246
|
+
parameters: Query parameters.
|
|
247
|
+
connection: Optional connection to use.
|
|
248
|
+
schema_type: Optional schema class for the result.
|
|
249
|
+
**kwargs: Additional keyword arguments.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
The first row of the query results.
|
|
253
|
+
"""
|
|
254
|
+
connection = self._connection(connection)
|
|
255
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
256
|
+
parameters = parameters if parameters is not None else {}
|
|
257
|
+
result = await connection.fetchrow(sql, *parameters) # pyright: ignore
|
|
258
|
+
result = self.check_not_found(result)
|
|
259
|
+
|
|
260
|
+
if schema_type is None:
|
|
261
|
+
# Always return as dictionary
|
|
262
|
+
return dict(result.items()) # type: ignore[attr-defined]
|
|
263
|
+
return cast("ModelDTOT", schema_type(**dict(result.items()))) # type: ignore[attr-defined]
|
|
264
|
+
|
|
265
|
+
async def select_one_or_none(
|
|
266
|
+
self,
|
|
267
|
+
sql: str,
|
|
268
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
269
|
+
/,
|
|
270
|
+
*,
|
|
271
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
272
|
+
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
273
|
+
**kwargs: Any,
|
|
274
|
+
) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
|
|
275
|
+
"""Fetch one row from the database.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
sql: SQL statement.
|
|
279
|
+
parameters: Query parameters.
|
|
280
|
+
connection: Optional connection to use.
|
|
281
|
+
schema_type: Optional schema class for the result.
|
|
282
|
+
**kwargs: Additional keyword arguments.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
The first row of the query results.
|
|
286
|
+
"""
|
|
287
|
+
connection = self._connection(connection)
|
|
288
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
289
|
+
parameters = parameters if parameters is not None else {}
|
|
290
|
+
result = await connection.fetchrow(sql, *parameters) # pyright: ignore
|
|
291
|
+
if result is None:
|
|
292
|
+
return None
|
|
293
|
+
if schema_type is None:
|
|
294
|
+
# Always return as dictionary
|
|
295
|
+
return dict(result.items())
|
|
296
|
+
return cast("ModelDTOT", schema_type(**dict(result.items())))
|
|
297
|
+
|
|
298
|
+
async def select_value(
|
|
299
|
+
self,
|
|
300
|
+
sql: str,
|
|
301
|
+
parameters: "Optional[StatementParameterType]" = None,
|
|
302
|
+
/,
|
|
303
|
+
*,
|
|
304
|
+
connection: "Optional[AsyncpgConnection]" = None,
|
|
305
|
+
schema_type: "Optional[type[T]]" = None,
|
|
306
|
+
**kwargs: Any,
|
|
307
|
+
) -> "Union[T, Any]":
|
|
308
|
+
"""Fetch a single value from the database.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
sql: SQL statement.
|
|
312
|
+
parameters: Query parameters.
|
|
313
|
+
connection: Optional connection to use.
|
|
314
|
+
schema_type: Optional schema class for the result.
|
|
315
|
+
**kwargs: Additional keyword arguments.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The first value from the first row of results, or None if no results.
|
|
319
|
+
"""
|
|
320
|
+
connection = self._connection(connection)
|
|
321
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
322
|
+
parameters = parameters if parameters is not None else {}
|
|
323
|
+
result = await connection.fetchval(sql, *parameters) # pyright: ignore
|
|
324
|
+
result = self.check_not_found(result)
|
|
325
|
+
if schema_type is None:
|
|
326
|
+
return result
|
|
327
|
+
return schema_type(result) # type: ignore[call-arg]
|
|
328
|
+
|
|
329
|
+
async def select_value_or_none(
|
|
330
|
+
self,
|
|
331
|
+
sql: str,
|
|
332
|
+
parameters: "Optional[StatementParameterType]" = None,
|
|
333
|
+
/,
|
|
334
|
+
*,
|
|
335
|
+
connection: "Optional[AsyncpgConnection]" = None,
|
|
336
|
+
schema_type: "Optional[type[T]]" = None,
|
|
337
|
+
**kwargs: Any,
|
|
338
|
+
) -> "Optional[Union[T, Any]]":
|
|
339
|
+
"""Fetch a single value from the database.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
The first value from the first row of results, or None if no results.
|
|
343
|
+
"""
|
|
344
|
+
connection = self._connection(connection)
|
|
345
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
346
|
+
parameters = parameters if parameters is not None else {}
|
|
347
|
+
result = await connection.fetchval(sql, *parameters) # pyright: ignore
|
|
348
|
+
if result is None:
|
|
349
|
+
return None
|
|
350
|
+
if schema_type is None:
|
|
351
|
+
return result
|
|
352
|
+
return schema_type(result) # type: ignore[call-arg]
|
|
353
|
+
|
|
354
|
+
async def insert_update_delete(
|
|
355
|
+
self,
|
|
356
|
+
sql: str,
|
|
357
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
358
|
+
/,
|
|
359
|
+
*,
|
|
360
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
361
|
+
**kwargs: Any,
|
|
362
|
+
) -> int:
|
|
363
|
+
"""Insert, update, or delete data from the database.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
sql: SQL statement.
|
|
367
|
+
parameters: Query parameters.
|
|
368
|
+
connection: Optional connection to use.
|
|
369
|
+
**kwargs: Additional keyword arguments.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Row count affected by the operation.
|
|
373
|
+
"""
|
|
374
|
+
connection = self._connection(connection)
|
|
375
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
376
|
+
parameters = parameters if parameters is not None else {}
|
|
377
|
+
status = await connection.execute(sql, *parameters) # pyright: ignore
|
|
378
|
+
# AsyncPG returns a string like "INSERT 0 1" where the last number is the affected rows
|
|
379
|
+
try:
|
|
380
|
+
return int(status.split()[-1]) # pyright: ignore[reportUnknownMemberType]
|
|
381
|
+
except (ValueError, IndexError, AttributeError):
|
|
382
|
+
return -1 # Fallback if we can't parse the status
|
|
383
|
+
|
|
384
|
+
async def insert_update_delete_returning(
|
|
385
|
+
self,
|
|
386
|
+
sql: str,
|
|
387
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
388
|
+
/,
|
|
389
|
+
*,
|
|
390
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
391
|
+
schema_type: "Optional[type[ModelDTOT]]" = None,
|
|
392
|
+
**kwargs: Any,
|
|
393
|
+
) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
|
|
394
|
+
"""Insert, update, or delete data from the database and return the affected row.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
sql: SQL statement.
|
|
398
|
+
parameters: Query parameters.
|
|
399
|
+
connection: Optional connection to use.
|
|
400
|
+
schema_type: Optional schema class for the result.
|
|
401
|
+
**kwargs: Additional keyword arguments.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
The affected row data as either a model instance or dictionary.
|
|
405
|
+
"""
|
|
406
|
+
connection = self._connection(connection)
|
|
407
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
408
|
+
parameters = parameters if parameters is not None else {}
|
|
409
|
+
result = await connection.fetchrow(sql, *parameters) # pyright: ignore
|
|
410
|
+
if result is None:
|
|
411
|
+
return None
|
|
412
|
+
if schema_type is None:
|
|
413
|
+
# Always return as dictionary
|
|
414
|
+
return dict(result.items()) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
415
|
+
return cast("ModelDTOT", schema_type(**dict(result.items()))) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType, reportUnknownVariableType]
|
|
416
|
+
|
|
417
|
+
async def execute_script(
|
|
418
|
+
self,
|
|
419
|
+
sql: str,
|
|
420
|
+
parameters: Optional["StatementParameterType"] = None,
|
|
421
|
+
/,
|
|
422
|
+
*,
|
|
423
|
+
connection: Optional["AsyncpgConnection"] = None,
|
|
424
|
+
**kwargs: Any,
|
|
425
|
+
) -> str:
|
|
426
|
+
"""Execute a script.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
sql: SQL statement.
|
|
430
|
+
parameters: Query parameters.
|
|
431
|
+
connection: Optional connection to use.
|
|
432
|
+
**kwargs: Additional keyword arguments.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Status message for the operation.
|
|
436
|
+
"""
|
|
437
|
+
connection = self._connection(connection)
|
|
438
|
+
sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
|
|
439
|
+
parameters = parameters if parameters is not None else {}
|
|
440
|
+
return await connection.execute(sql, *parameters) # pyright: ignore
|
|
441
|
+
|
|
442
|
+
def _connection(self, connection: Optional["AsyncpgConnection"] = None) -> "AsyncpgConnection":
|
|
443
|
+
"""Return the connection to use. If None, use the default connection."""
|
|
444
|
+
return connection if connection is not None else self.connection
|