sqlspec 0.25.0__py3-none-any.whl → 0.27.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 +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/driver/_common.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Common driver attributes and utilities."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import re
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Final, NamedTuple, NoReturn, Optional, TypeVar, cast
|
|
4
6
|
|
|
5
7
|
from mypy_extensions import trait
|
|
6
8
|
from sqlglot import exp
|
|
@@ -9,7 +11,7 @@ from sqlspec.builder import QueryBuilder
|
|
|
9
11
|
from sqlspec.core import SQL, ParameterStyle, SQLResult, Statement, StatementConfig, TypedParameter
|
|
10
12
|
from sqlspec.core.cache import CachedStatement, get_cache, get_cache_config
|
|
11
13
|
from sqlspec.core.splitter import split_sql_script
|
|
12
|
-
from sqlspec.exceptions import ImproperConfigurationError
|
|
14
|
+
from sqlspec.exceptions import ImproperConfigurationError, NotFoundError
|
|
13
15
|
from sqlspec.utils.logging import get_logger
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
@@ -25,19 +27,229 @@ __all__ = (
|
|
|
25
27
|
"EXEC_ROWCOUNT_OVERRIDE",
|
|
26
28
|
"EXEC_SPECIAL_DATA",
|
|
27
29
|
"CommonDriverAttributesMixin",
|
|
30
|
+
"DataDictionaryMixin",
|
|
28
31
|
"ExecutionResult",
|
|
29
32
|
"ScriptExecutionResult",
|
|
33
|
+
"VersionInfo",
|
|
34
|
+
"handle_single_row_error",
|
|
35
|
+
"make_cache_key_hashable",
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
logger = get_logger("driver")
|
|
34
40
|
|
|
41
|
+
DriverT = TypeVar("DriverT")
|
|
42
|
+
VERSION_GROUPS_MIN_FOR_MINOR = 1
|
|
43
|
+
VERSION_GROUPS_MIN_FOR_PATCH = 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def make_cache_key_hashable(obj: Any) -> Any:
|
|
47
|
+
"""Recursively convert unhashable types to hashable ones for cache keys.
|
|
48
|
+
|
|
49
|
+
For array-like objects (NumPy arrays, Python arrays, etc.), we use structural
|
|
50
|
+
info (dtype + shape or typecode + length) rather than content for cache keys.
|
|
51
|
+
This ensures high cache hit rates for parameterized queries with different
|
|
52
|
+
vector values while avoiding expensive content hashing.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
obj: Object to make hashable.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A hashable representation of the object. Collections become tuples,
|
|
59
|
+
arrays become structural tuples like ("ndarray", dtype, shape).
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
>>> make_cache_key_hashable([1, 2, 3])
|
|
63
|
+
(1, 2, 3)
|
|
64
|
+
>>> make_cache_key_hashable({"a": 1, "b": 2})
|
|
65
|
+
(('a', 1), ('b', 2))
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(obj, (list, tuple)):
|
|
68
|
+
return tuple(make_cache_key_hashable(item) for item in obj)
|
|
69
|
+
if isinstance(obj, dict):
|
|
70
|
+
return tuple(sorted((k, make_cache_key_hashable(v)) for k, v in obj.items()))
|
|
71
|
+
if isinstance(obj, set):
|
|
72
|
+
return frozenset(make_cache_key_hashable(item) for item in obj)
|
|
73
|
+
|
|
74
|
+
typecode = getattr(obj, "typecode", None)
|
|
75
|
+
if typecode is not None:
|
|
76
|
+
try:
|
|
77
|
+
length = len(obj)
|
|
78
|
+
except (AttributeError, TypeError):
|
|
79
|
+
return ("array", typecode)
|
|
80
|
+
else:
|
|
81
|
+
return ("array", typecode, length)
|
|
82
|
+
|
|
83
|
+
if hasattr(obj, "__array__"):
|
|
84
|
+
try:
|
|
85
|
+
dtype_str = getattr(obj.dtype, "str", str(type(obj)))
|
|
86
|
+
shape = tuple(int(s) for s in obj.shape)
|
|
87
|
+
except (AttributeError, TypeError):
|
|
88
|
+
try:
|
|
89
|
+
length = len(obj)
|
|
90
|
+
except (AttributeError, TypeError):
|
|
91
|
+
return ("array_like", type(obj).__name__)
|
|
92
|
+
else:
|
|
93
|
+
return ("array_like", type(obj).__name__, length)
|
|
94
|
+
else:
|
|
95
|
+
return ("ndarray", dtype_str, shape)
|
|
96
|
+
return obj
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def handle_single_row_error(error: ValueError) -> "NoReturn":
|
|
100
|
+
"""Normalize single-row selection errors to SQLSpec exceptions."""
|
|
101
|
+
|
|
102
|
+
message = str(error)
|
|
103
|
+
if message.startswith("No result found"):
|
|
104
|
+
msg = "No rows found"
|
|
105
|
+
raise NotFoundError(msg) from error
|
|
106
|
+
raise error
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class VersionInfo:
|
|
110
|
+
"""Database version information."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None:
|
|
113
|
+
"""Initialize version info.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
major: Major version number
|
|
117
|
+
minor: Minor version number
|
|
118
|
+
patch: Patch version number
|
|
119
|
+
"""
|
|
120
|
+
self.major = major
|
|
121
|
+
self.minor = minor
|
|
122
|
+
self.patch = patch
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def version_tuple(self) -> "tuple[int, int, int]":
|
|
126
|
+
"""Get version as tuple for comparison."""
|
|
127
|
+
return (self.major, self.minor, self.patch)
|
|
128
|
+
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
"""String representation of version info."""
|
|
131
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
132
|
+
|
|
133
|
+
def __repr__(self) -> str:
|
|
134
|
+
"""Detailed string representation."""
|
|
135
|
+
return f"VersionInfo({self.major}, {self.minor}, {self.patch})"
|
|
136
|
+
|
|
137
|
+
def __eq__(self, other: object) -> bool:
|
|
138
|
+
"""Check version equality."""
|
|
139
|
+
if not isinstance(other, VersionInfo):
|
|
140
|
+
return NotImplemented
|
|
141
|
+
return self.version_tuple == other.version_tuple
|
|
142
|
+
|
|
143
|
+
def __lt__(self, other: "VersionInfo") -> bool:
|
|
144
|
+
"""Check if this version is less than another."""
|
|
145
|
+
return self.version_tuple < other.version_tuple
|
|
146
|
+
|
|
147
|
+
def __le__(self, other: "VersionInfo") -> bool:
|
|
148
|
+
"""Check if this version is less than or equal to another."""
|
|
149
|
+
return self.version_tuple <= other.version_tuple
|
|
150
|
+
|
|
151
|
+
def __gt__(self, other: "VersionInfo") -> bool:
|
|
152
|
+
"""Check if this version is greater than another."""
|
|
153
|
+
return self.version_tuple > other.version_tuple
|
|
154
|
+
|
|
155
|
+
def __ge__(self, other: "VersionInfo") -> bool:
|
|
156
|
+
"""Check if this version is greater than or equal to another."""
|
|
157
|
+
return self.version_tuple >= other.version_tuple
|
|
158
|
+
|
|
159
|
+
def __hash__(self) -> int:
|
|
160
|
+
"""Make VersionInfo hashable based on version tuple."""
|
|
161
|
+
return hash(self.version_tuple)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@trait
|
|
165
|
+
class DataDictionaryMixin:
|
|
166
|
+
"""Mixin providing common data dictionary functionality."""
|
|
167
|
+
|
|
168
|
+
def parse_version_string(self, version_str: str) -> "VersionInfo | None":
|
|
169
|
+
"""Parse version string into VersionInfo.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
version_str: Raw version string from database
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
VersionInfo instance or None if parsing fails
|
|
176
|
+
"""
|
|
177
|
+
# Try common version patterns
|
|
178
|
+
patterns = [
|
|
179
|
+
r"(\d+)\.(\d+)\.(\d+)", # x.y.z
|
|
180
|
+
r"(\d+)\.(\d+)", # x.y
|
|
181
|
+
r"(\d+)", # x
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
for pattern in patterns:
|
|
185
|
+
match = re.search(pattern, version_str)
|
|
186
|
+
if match:
|
|
187
|
+
groups = match.groups()
|
|
188
|
+
|
|
189
|
+
major = int(groups[0])
|
|
190
|
+
minor = int(groups[1]) if len(groups) > VERSION_GROUPS_MIN_FOR_MINOR else 0
|
|
191
|
+
patch = int(groups[2]) if len(groups) > VERSION_GROUPS_MIN_FOR_PATCH else 0
|
|
192
|
+
return VersionInfo(major, minor, patch)
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "VersionInfo | None":
|
|
197
|
+
"""Try multiple version queries to detect database version.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
driver: Database driver instance
|
|
201
|
+
queries: List of SQL queries to try
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Version information or None if detection fails
|
|
205
|
+
"""
|
|
206
|
+
for query in queries:
|
|
207
|
+
with suppress(Exception):
|
|
208
|
+
result = driver.execute(query)
|
|
209
|
+
if result.data:
|
|
210
|
+
version_str = str(result.data[0])
|
|
211
|
+
if isinstance(result.data[0], dict):
|
|
212
|
+
version_str = str(next(iter(result.data[0].values())))
|
|
213
|
+
elif isinstance(result.data[0], (list, tuple)):
|
|
214
|
+
version_str = str(result.data[0][0])
|
|
215
|
+
|
|
216
|
+
parsed_version = self.parse_version_string(version_str)
|
|
217
|
+
if parsed_version:
|
|
218
|
+
logger.debug("Detected database version: %s", parsed_version)
|
|
219
|
+
return parsed_version
|
|
220
|
+
|
|
221
|
+
logger.warning("Could not detect database version")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def get_default_type_mapping(self) -> "dict[str, str]":
|
|
225
|
+
"""Get default type mappings for common categories.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionary mapping type categories to generic SQL types
|
|
229
|
+
"""
|
|
230
|
+
return {
|
|
231
|
+
"json": "TEXT",
|
|
232
|
+
"uuid": "VARCHAR(36)",
|
|
233
|
+
"boolean": "INTEGER",
|
|
234
|
+
"timestamp": "TIMESTAMP",
|
|
235
|
+
"text": "TEXT",
|
|
236
|
+
"blob": "BLOB",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def get_default_features(self) -> "list[str]":
|
|
240
|
+
"""Get default feature flags supported by most databases.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of commonly supported feature names
|
|
244
|
+
"""
|
|
245
|
+
return ["supports_transactions", "supports_prepared_statements"]
|
|
246
|
+
|
|
35
247
|
|
|
36
248
|
class ScriptExecutionResult(NamedTuple):
|
|
37
249
|
"""Result from script execution with statement count information."""
|
|
38
250
|
|
|
39
251
|
cursor_result: Any
|
|
40
|
-
rowcount_override:
|
|
252
|
+
rowcount_override: int | None
|
|
41
253
|
special_data: Any
|
|
42
254
|
statement_count: int
|
|
43
255
|
successful_statements: int
|
|
@@ -47,23 +259,23 @@ class ExecutionResult(NamedTuple):
|
|
|
47
259
|
"""Execution result containing all data needed for SQLResult building."""
|
|
48
260
|
|
|
49
261
|
cursor_result: Any
|
|
50
|
-
rowcount_override:
|
|
262
|
+
rowcount_override: int | None
|
|
51
263
|
special_data: Any
|
|
52
264
|
selected_data: Optional["list[dict[str, Any]]"]
|
|
53
265
|
column_names: Optional["list[str]"]
|
|
54
|
-
data_row_count:
|
|
55
|
-
statement_count:
|
|
56
|
-
successful_statements:
|
|
266
|
+
data_row_count: int | None
|
|
267
|
+
statement_count: int | None
|
|
268
|
+
successful_statements: int | None
|
|
57
269
|
is_script_result: bool
|
|
58
270
|
is_select_result: bool
|
|
59
271
|
is_many_result: bool
|
|
60
|
-
last_inserted_id:
|
|
272
|
+
last_inserted_id: int | str | None = None
|
|
61
273
|
|
|
62
274
|
|
|
63
275
|
EXEC_CURSOR_RESULT: Final[int] = 0
|
|
64
276
|
EXEC_ROWCOUNT_OVERRIDE: Final[int] = 1
|
|
65
277
|
EXEC_SPECIAL_DATA: Final[int] = 2
|
|
66
|
-
DEFAULT_EXECUTION_RESULT: Final[tuple[Any,
|
|
278
|
+
DEFAULT_EXECUTION_RESULT: Final[tuple[Any, int | None, Any]] = (None, None, None)
|
|
67
279
|
|
|
68
280
|
|
|
69
281
|
@trait
|
|
@@ -76,7 +288,7 @@ class CommonDriverAttributesMixin:
|
|
|
76
288
|
driver_features: "dict[str, Any]"
|
|
77
289
|
|
|
78
290
|
def __init__(
|
|
79
|
-
self, connection: "Any", statement_config: "StatementConfig", driver_features: "
|
|
291
|
+
self, connection: "Any", statement_config: "StatementConfig", driver_features: "dict[str, Any] | None" = None
|
|
80
292
|
) -> None:
|
|
81
293
|
"""Initialize driver adapter with connection and configuration.
|
|
82
294
|
|
|
@@ -93,17 +305,17 @@ class CommonDriverAttributesMixin:
|
|
|
93
305
|
self,
|
|
94
306
|
cursor_result: Any,
|
|
95
307
|
*,
|
|
96
|
-
rowcount_override:
|
|
308
|
+
rowcount_override: int | None = None,
|
|
97
309
|
special_data: Any = None,
|
|
98
310
|
selected_data: Optional["list[dict[str, Any]]"] = None,
|
|
99
311
|
column_names: Optional["list[str]"] = None,
|
|
100
|
-
data_row_count:
|
|
101
|
-
statement_count:
|
|
102
|
-
successful_statements:
|
|
312
|
+
data_row_count: int | None = None,
|
|
313
|
+
statement_count: int | None = None,
|
|
314
|
+
successful_statements: int | None = None,
|
|
103
315
|
is_script_result: bool = False,
|
|
104
316
|
is_select_result: bool = False,
|
|
105
317
|
is_many_result: bool = False,
|
|
106
|
-
last_inserted_id:
|
|
318
|
+
last_inserted_id: int | str | None = None,
|
|
107
319
|
) -> ExecutionResult:
|
|
108
320
|
"""Create ExecutionResult with all necessary data for any operation type.
|
|
109
321
|
|
|
@@ -181,11 +393,11 @@ class CommonDriverAttributesMixin:
|
|
|
181
393
|
|
|
182
394
|
def prepare_statement(
|
|
183
395
|
self,
|
|
184
|
-
statement: "
|
|
185
|
-
parameters: "tuple[
|
|
396
|
+
statement: "Statement | QueryBuilder",
|
|
397
|
+
parameters: "tuple[StatementParameters | StatementFilter, ...]" = (),
|
|
186
398
|
*,
|
|
187
399
|
statement_config: "StatementConfig",
|
|
188
|
-
kwargs: "
|
|
400
|
+
kwargs: "dict[str, Any] | None" = None,
|
|
189
401
|
) -> "SQL":
|
|
190
402
|
"""Build SQL statement from various input types.
|
|
191
403
|
|
|
@@ -270,7 +482,11 @@ class CommonDriverAttributesMixin:
|
|
|
270
482
|
]
|
|
271
483
|
|
|
272
484
|
def prepare_driver_parameters(
|
|
273
|
-
self,
|
|
485
|
+
self,
|
|
486
|
+
parameters: Any,
|
|
487
|
+
statement_config: "StatementConfig",
|
|
488
|
+
is_many: bool = False,
|
|
489
|
+
prepared_statement: Any | None = None, # pyright: ignore[reportUnusedParameter]
|
|
274
490
|
) -> Any:
|
|
275
491
|
"""Prepare parameters for database driver consumption.
|
|
276
492
|
|
|
@@ -281,6 +497,7 @@ class CommonDriverAttributesMixin:
|
|
|
281
497
|
parameters: Parameters in any format (dict, list, tuple, scalar, TypedParameter)
|
|
282
498
|
statement_config: Statement configuration for parameter style detection
|
|
283
499
|
is_many: If True, handle as executemany parameter sequence
|
|
500
|
+
prepared_statement: Optional prepared statement containing metadata for parameter processing
|
|
284
501
|
|
|
285
502
|
Returns:
|
|
286
503
|
Parameters with TypedParameter objects unwrapped to primitive values
|
|
@@ -297,6 +514,23 @@ class CommonDriverAttributesMixin:
|
|
|
297
514
|
return [self._format_parameter_set_for_many(parameters, statement_config)]
|
|
298
515
|
return self._format_parameter_set(parameters, statement_config)
|
|
299
516
|
|
|
517
|
+
def _apply_coercion(self, value: Any, statement_config: "StatementConfig") -> Any:
|
|
518
|
+
"""Apply type coercion to a single value.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
value: Value to coerce (may be TypedParameter or raw value)
|
|
522
|
+
statement_config: Statement configuration for type coercion map
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Coerced value with TypedParameter unwrapped
|
|
526
|
+
"""
|
|
527
|
+
unwrapped_value = value.value if isinstance(value, TypedParameter) else value
|
|
528
|
+
if statement_config.parameter_config.type_coercion_map:
|
|
529
|
+
for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
|
|
530
|
+
if isinstance(unwrapped_value, type_check):
|
|
531
|
+
return converter(unwrapped_value)
|
|
532
|
+
return unwrapped_value
|
|
533
|
+
|
|
300
534
|
def _format_parameter_set_for_many(self, parameters: Any, statement_config: "StatementConfig") -> Any:
|
|
301
535
|
"""Prepare a single parameter set for execute_many operations.
|
|
302
536
|
|
|
@@ -313,27 +547,14 @@ class CommonDriverAttributesMixin:
|
|
|
313
547
|
if not parameters:
|
|
314
548
|
return []
|
|
315
549
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
unwrapped_value = value.value if isinstance(value, TypedParameter) else value
|
|
319
|
-
|
|
320
|
-
if statement_config.parameter_config.type_coercion_map:
|
|
321
|
-
for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
|
|
322
|
-
if type_check in {list, tuple} and isinstance(unwrapped_value, (list, tuple)):
|
|
323
|
-
continue
|
|
324
|
-
if isinstance(unwrapped_value, type_check):
|
|
325
|
-
return converter(unwrapped_value)
|
|
326
|
-
|
|
327
|
-
return unwrapped_value
|
|
550
|
+
if not isinstance(parameters, (dict, list, tuple)):
|
|
551
|
+
return self._apply_coercion(parameters, statement_config)
|
|
328
552
|
|
|
329
553
|
if isinstance(parameters, dict):
|
|
330
|
-
return {k:
|
|
331
|
-
|
|
332
|
-
if isinstance(parameters, (list, tuple)):
|
|
333
|
-
coerced_params = [apply_type_coercion(p) for p in parameters]
|
|
334
|
-
return tuple(coerced_params) if isinstance(parameters, tuple) else coerced_params
|
|
554
|
+
return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
|
|
335
555
|
|
|
336
|
-
|
|
556
|
+
coerced_params = [self._apply_coercion(p, statement_config) for p in parameters]
|
|
557
|
+
return tuple(coerced_params) if isinstance(parameters, tuple) else coerced_params
|
|
337
558
|
|
|
338
559
|
def _format_parameter_set(self, parameters: Any, statement_config: "StatementConfig") -> Any:
|
|
339
560
|
"""Prepare a single parameter set for database driver consumption.
|
|
@@ -348,50 +569,34 @@ class CommonDriverAttributesMixin:
|
|
|
348
569
|
if not parameters:
|
|
349
570
|
return []
|
|
350
571
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
unwrapped_value = value.value if isinstance(value, TypedParameter) else value
|
|
354
|
-
|
|
355
|
-
if statement_config.parameter_config.type_coercion_map:
|
|
356
|
-
for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
|
|
357
|
-
if isinstance(unwrapped_value, type_check):
|
|
358
|
-
return converter(unwrapped_value)
|
|
359
|
-
|
|
360
|
-
return unwrapped_value
|
|
572
|
+
if not isinstance(parameters, (dict, list, tuple)):
|
|
573
|
+
return [self._apply_coercion(parameters, statement_config)]
|
|
361
574
|
|
|
362
575
|
if isinstance(parameters, dict):
|
|
363
|
-
if not parameters:
|
|
364
|
-
return []
|
|
365
576
|
if statement_config.parameter_config.supported_execution_parameter_styles and (
|
|
366
577
|
ParameterStyle.NAMED_PYFORMAT in statement_config.parameter_config.supported_execution_parameter_styles
|
|
367
578
|
or ParameterStyle.NAMED_COLON in statement_config.parameter_config.supported_execution_parameter_styles
|
|
368
579
|
):
|
|
369
|
-
return {k:
|
|
580
|
+
return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
|
|
370
581
|
if statement_config.parameter_config.default_parameter_style in {
|
|
371
582
|
ParameterStyle.NUMERIC,
|
|
372
583
|
ParameterStyle.QMARK,
|
|
373
584
|
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
374
585
|
}:
|
|
375
|
-
ordered_parameters = []
|
|
376
586
|
sorted_items = sorted(
|
|
377
587
|
parameters.items(),
|
|
378
588
|
key=lambda item: int(item[0])
|
|
379
589
|
if item[0].isdigit()
|
|
380
590
|
else (int(item[0][6:]) if item[0].startswith("param_") and item[0][6:].isdigit() else float("inf")),
|
|
381
591
|
)
|
|
382
|
-
for _, value in sorted_items
|
|
383
|
-
ordered_parameters.append(apply_type_coercion(value))
|
|
384
|
-
return ordered_parameters
|
|
592
|
+
return [self._apply_coercion(value, statement_config) for _, value in sorted_items]
|
|
385
593
|
|
|
386
|
-
return {k:
|
|
594
|
+
return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
|
|
387
595
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return coerced_params
|
|
393
|
-
|
|
394
|
-
return [apply_type_coercion(parameters)]
|
|
596
|
+
coerced_params = [self._apply_coercion(p, statement_config) for p in parameters]
|
|
597
|
+
if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
|
|
598
|
+
return tuple(coerced_params)
|
|
599
|
+
return coerced_params
|
|
395
600
|
|
|
396
601
|
def _get_compiled_sql(
|
|
397
602
|
self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
|
|
@@ -422,7 +627,7 @@ class CommonDriverAttributesMixin:
|
|
|
422
627
|
compiled_sql, execution_parameters = prepared_statement.compile()
|
|
423
628
|
|
|
424
629
|
prepared_parameters = self.prepare_driver_parameters(
|
|
425
|
-
execution_parameters, statement_config, is_many=statement.is_many
|
|
630
|
+
execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement
|
|
426
631
|
)
|
|
427
632
|
|
|
428
633
|
if statement_config.parameter_config.output_transformer:
|
|
@@ -438,7 +643,7 @@ class CommonDriverAttributesMixin:
|
|
|
438
643
|
if isinstance(prepared_parameters, list)
|
|
439
644
|
else (
|
|
440
645
|
prepared_parameters
|
|
441
|
-
if prepared_parameters is None
|
|
646
|
+
if prepared_parameters is None or isinstance(prepared_parameters, dict)
|
|
442
647
|
else (
|
|
443
648
|
tuple(prepared_parameters)
|
|
444
649
|
if not isinstance(prepared_parameters, tuple)
|
|
@@ -473,26 +678,26 @@ class CommonDriverAttributesMixin:
|
|
|
473
678
|
)
|
|
474
679
|
|
|
475
680
|
params = statement.parameters
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
681
|
+
|
|
682
|
+
if params is None or (isinstance(params, (list, tuple, dict)) and not params):
|
|
683
|
+
return f"compiled:{hash(statement.sql)}:{context_hash}"
|
|
684
|
+
|
|
685
|
+
if isinstance(params, tuple) and all(isinstance(p, (int, str, bytes, bool, type(None))) for p in params):
|
|
686
|
+
try:
|
|
687
|
+
return (
|
|
688
|
+
f"compiled:{hash((statement.sql, params, statement.is_many, statement.is_script))}:{context_hash}"
|
|
689
|
+
)
|
|
690
|
+
except TypeError:
|
|
691
|
+
pass
|
|
487
692
|
|
|
488
693
|
try:
|
|
489
694
|
if isinstance(params, dict):
|
|
490
|
-
params_key =
|
|
695
|
+
params_key = make_cache_key_hashable(params)
|
|
491
696
|
elif isinstance(params, (list, tuple)) and params:
|
|
492
697
|
if isinstance(params[0], dict):
|
|
493
|
-
params_key = tuple(
|
|
698
|
+
params_key = tuple(make_cache_key_hashable(d) for d in params)
|
|
494
699
|
else:
|
|
495
|
-
params_key =
|
|
700
|
+
params_key = make_cache_key_hashable(params)
|
|
496
701
|
elif isinstance(params, (list, tuple)):
|
|
497
702
|
params_key = ()
|
|
498
703
|
else:
|
|
@@ -503,7 +708,7 @@ class CommonDriverAttributesMixin:
|
|
|
503
708
|
base_hash = hash((statement.sql, params_key, statement.is_many, statement.is_script))
|
|
504
709
|
return f"compiled:{base_hash}:{context_hash}"
|
|
505
710
|
|
|
506
|
-
def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "
|
|
711
|
+
def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "ParameterStyle | None":
|
|
507
712
|
"""Determine the dominant parameter style from parameter info list.
|
|
508
713
|
|
|
509
714
|
Args:
|
|
@@ -536,7 +741,7 @@ class CommonDriverAttributesMixin:
|
|
|
536
741
|
def find_filter(
|
|
537
742
|
filter_type: "type[FilterTypeT]",
|
|
538
743
|
filters: "Sequence[StatementFilter | StatementParameters] | Sequence[StatementFilter]",
|
|
539
|
-
) -> "
|
|
744
|
+
) -> "FilterTypeT | None":
|
|
540
745
|
"""Get the filter specified by filter type from the filters.
|
|
541
746
|
|
|
542
747
|
Args:
|