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.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {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
- from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, Union, cast
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: Optional[int]
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: Optional[int]
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: Optional[int]
55
- statement_count: Optional[int]
56
- successful_statements: Optional[int]
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: Optional[Union[int, str]] = None
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, Optional[int], Any]] = (None, None, None)
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: "Optional[dict[str, Any]]" = None
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: Optional[int] = None,
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: Optional[int] = None,
101
- statement_count: Optional[int] = None,
102
- successful_statements: Optional[int] = None,
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: Optional[Union[int, str]] = None,
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: "Union[Statement, QueryBuilder]",
185
- parameters: "tuple[Union[StatementParameters, StatementFilter], ...]" = (),
396
+ statement: "Statement | QueryBuilder",
397
+ parameters: "tuple[StatementParameters | StatementFilter, ...]" = (),
186
398
  *,
187
399
  statement_config: "StatementConfig",
188
- kwargs: "Optional[dict[str, Any]]" = None,
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, parameters: Any, statement_config: "StatementConfig", is_many: bool = False
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
- def apply_type_coercion(value: Any) -> Any:
317
- """Apply type coercion to a single value."""
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: apply_type_coercion(v) for k, v in parameters.items()}
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
- return apply_type_coercion(parameters)
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
- def apply_type_coercion(value: Any) -> Any:
352
- """Apply type coercion to a single value."""
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: apply_type_coercion(v) for k, v in parameters.items()}
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: apply_type_coercion(v) for k, v in parameters.items()}
594
+ return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
387
595
 
388
- if isinstance(parameters, (list, tuple)):
389
- coerced_params = [apply_type_coercion(p) for p in parameters]
390
- if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
391
- return tuple(coerced_params)
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
- params_key: Any
477
-
478
- def make_hashable(obj: Any) -> Any:
479
- """Recursively convert unhashable types to hashable ones."""
480
- if isinstance(obj, (list, tuple)):
481
- return tuple(make_hashable(item) for item in obj)
482
- if isinstance(obj, dict):
483
- return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
484
- if isinstance(obj, set):
485
- return frozenset(make_hashable(item) for item in obj)
486
- return obj
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 = make_hashable(params)
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(make_hashable(d) for d in params)
698
+ params_key = tuple(make_cache_key_hashable(d) for d in params)
494
699
  else:
495
- params_key = make_hashable(params)
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]") -> "Optional[ParameterStyle]":
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
- ) -> "Optional[FilterTypeT]":
744
+ ) -> "FilterTypeT | None":
540
745
  """Get the filter specified by filter type from the filters.
541
746
 
542
747
  Args: