sqlspec 0.26.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 (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -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 +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  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 +44 -50
  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 +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  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 +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  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 +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  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 +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  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 +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  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 +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  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 +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  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 +40 -11
  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 +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  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 +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -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/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/driver/_common.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from contextlib import suppress
5
- from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, TypeVar, Union, cast
5
+ from typing import TYPE_CHECKING, Any, Final, NamedTuple, NoReturn, Optional, TypeVar, cast
6
6
 
7
7
  from mypy_extensions import trait
8
8
  from sqlglot import exp
@@ -11,7 +11,7 @@ from sqlspec.builder import QueryBuilder
11
11
  from sqlspec.core import SQL, ParameterStyle, SQLResult, Statement, StatementConfig, TypedParameter
12
12
  from sqlspec.core.cache import CachedStatement, get_cache, get_cache_config
13
13
  from sqlspec.core.splitter import split_sql_script
14
- from sqlspec.exceptions import ImproperConfigurationError
14
+ from sqlspec.exceptions import ImproperConfigurationError, NotFoundError
15
15
  from sqlspec.utils.logging import get_logger
16
16
 
17
17
  if TYPE_CHECKING:
@@ -31,6 +31,8 @@ __all__ = (
31
31
  "ExecutionResult",
32
32
  "ScriptExecutionResult",
33
33
  "VersionInfo",
34
+ "handle_single_row_error",
35
+ "make_cache_key_hashable",
34
36
  )
35
37
 
36
38
 
@@ -41,6 +43,69 @@ VERSION_GROUPS_MIN_FOR_MINOR = 1
41
43
  VERSION_GROUPS_MIN_FOR_PATCH = 2
42
44
 
43
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
+
44
109
  class VersionInfo:
45
110
  """Database version information."""
46
111
 
@@ -100,7 +165,7 @@ class VersionInfo:
100
165
  class DataDictionaryMixin:
101
166
  """Mixin providing common data dictionary functionality."""
102
167
 
103
- def parse_version_string(self, version_str: str) -> "Optional[VersionInfo]":
168
+ def parse_version_string(self, version_str: str) -> "VersionInfo | None":
104
169
  """Parse version string into VersionInfo.
105
170
 
106
171
  Args:
@@ -128,7 +193,7 @@ class DataDictionaryMixin:
128
193
 
129
194
  return None
130
195
 
131
- def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "Optional[VersionInfo]":
196
+ def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "VersionInfo | None":
132
197
  """Try multiple version queries to detect database version.
133
198
 
134
199
  Args:
@@ -184,7 +249,7 @@ class ScriptExecutionResult(NamedTuple):
184
249
  """Result from script execution with statement count information."""
185
250
 
186
251
  cursor_result: Any
187
- rowcount_override: Optional[int]
252
+ rowcount_override: int | None
188
253
  special_data: Any
189
254
  statement_count: int
190
255
  successful_statements: int
@@ -194,23 +259,23 @@ class ExecutionResult(NamedTuple):
194
259
  """Execution result containing all data needed for SQLResult building."""
195
260
 
196
261
  cursor_result: Any
197
- rowcount_override: Optional[int]
262
+ rowcount_override: int | None
198
263
  special_data: Any
199
264
  selected_data: Optional["list[dict[str, Any]]"]
200
265
  column_names: Optional["list[str]"]
201
- data_row_count: Optional[int]
202
- statement_count: Optional[int]
203
- successful_statements: Optional[int]
266
+ data_row_count: int | None
267
+ statement_count: int | None
268
+ successful_statements: int | None
204
269
  is_script_result: bool
205
270
  is_select_result: bool
206
271
  is_many_result: bool
207
- last_inserted_id: Optional[Union[int, str]] = None
272
+ last_inserted_id: int | str | None = None
208
273
 
209
274
 
210
275
  EXEC_CURSOR_RESULT: Final[int] = 0
211
276
  EXEC_ROWCOUNT_OVERRIDE: Final[int] = 1
212
277
  EXEC_SPECIAL_DATA: Final[int] = 2
213
- 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)
214
279
 
215
280
 
216
281
  @trait
@@ -223,7 +288,7 @@ class CommonDriverAttributesMixin:
223
288
  driver_features: "dict[str, Any]"
224
289
 
225
290
  def __init__(
226
- 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
227
292
  ) -> None:
228
293
  """Initialize driver adapter with connection and configuration.
229
294
 
@@ -240,17 +305,17 @@ class CommonDriverAttributesMixin:
240
305
  self,
241
306
  cursor_result: Any,
242
307
  *,
243
- rowcount_override: Optional[int] = None,
308
+ rowcount_override: int | None = None,
244
309
  special_data: Any = None,
245
310
  selected_data: Optional["list[dict[str, Any]]"] = None,
246
311
  column_names: Optional["list[str]"] = None,
247
- data_row_count: Optional[int] = None,
248
- statement_count: Optional[int] = None,
249
- successful_statements: Optional[int] = None,
312
+ data_row_count: int | None = None,
313
+ statement_count: int | None = None,
314
+ successful_statements: int | None = None,
250
315
  is_script_result: bool = False,
251
316
  is_select_result: bool = False,
252
317
  is_many_result: bool = False,
253
- last_inserted_id: Optional[Union[int, str]] = None,
318
+ last_inserted_id: int | str | None = None,
254
319
  ) -> ExecutionResult:
255
320
  """Create ExecutionResult with all necessary data for any operation type.
256
321
 
@@ -328,11 +393,11 @@ class CommonDriverAttributesMixin:
328
393
 
329
394
  def prepare_statement(
330
395
  self,
331
- statement: "Union[Statement, QueryBuilder]",
332
- parameters: "tuple[Union[StatementParameters, StatementFilter], ...]" = (),
396
+ statement: "Statement | QueryBuilder",
397
+ parameters: "tuple[StatementParameters | StatementFilter, ...]" = (),
333
398
  *,
334
399
  statement_config: "StatementConfig",
335
- kwargs: "Optional[dict[str, Any]]" = None,
400
+ kwargs: "dict[str, Any] | None" = None,
336
401
  ) -> "SQL":
337
402
  """Build SQL statement from various input types.
338
403
 
@@ -421,7 +486,7 @@ class CommonDriverAttributesMixin:
421
486
  parameters: Any,
422
487
  statement_config: "StatementConfig",
423
488
  is_many: bool = False,
424
- prepared_statement: Optional[Any] = None, # pyright: ignore[reportUnusedParameter]
489
+ prepared_statement: Any | None = None, # pyright: ignore[reportUnusedParameter]
425
490
  ) -> Any:
426
491
  """Prepare parameters for database driver consumption.
427
492
 
@@ -449,6 +514,23 @@ class CommonDriverAttributesMixin:
449
514
  return [self._format_parameter_set_for_many(parameters, statement_config)]
450
515
  return self._format_parameter_set(parameters, statement_config)
451
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
+
452
534
  def _format_parameter_set_for_many(self, parameters: Any, statement_config: "StatementConfig") -> Any:
453
535
  """Prepare a single parameter set for execute_many operations.
454
536
 
@@ -465,27 +547,14 @@ class CommonDriverAttributesMixin:
465
547
  if not parameters:
466
548
  return []
467
549
 
468
- def apply_type_coercion(value: Any) -> Any:
469
- """Apply type coercion to a single value."""
470
- unwrapped_value = value.value if isinstance(value, TypedParameter) else value
471
-
472
- if statement_config.parameter_config.type_coercion_map:
473
- for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
474
- if type_check in {list, tuple} and isinstance(unwrapped_value, (list, tuple)):
475
- continue
476
- if isinstance(unwrapped_value, type_check):
477
- return converter(unwrapped_value)
478
-
479
- return unwrapped_value
550
+ if not isinstance(parameters, (dict, list, tuple)):
551
+ return self._apply_coercion(parameters, statement_config)
480
552
 
481
553
  if isinstance(parameters, dict):
482
- return {k: apply_type_coercion(v) for k, v in parameters.items()}
483
-
484
- if isinstance(parameters, (list, tuple)):
485
- coerced_params = [apply_type_coercion(p) for p in parameters]
486
- 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()}
487
555
 
488
- 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
489
558
 
490
559
  def _format_parameter_set(self, parameters: Any, statement_config: "StatementConfig") -> Any:
491
560
  """Prepare a single parameter set for database driver consumption.
@@ -500,50 +569,34 @@ class CommonDriverAttributesMixin:
500
569
  if not parameters:
501
570
  return []
502
571
 
503
- def apply_type_coercion(value: Any) -> Any:
504
- """Apply type coercion to a single value."""
505
- unwrapped_value = value.value if isinstance(value, TypedParameter) else value
506
-
507
- if statement_config.parameter_config.type_coercion_map:
508
- for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
509
- if isinstance(unwrapped_value, type_check):
510
- return converter(unwrapped_value)
511
-
512
- return unwrapped_value
572
+ if not isinstance(parameters, (dict, list, tuple)):
573
+ return [self._apply_coercion(parameters, statement_config)]
513
574
 
514
575
  if isinstance(parameters, dict):
515
- if not parameters:
516
- return []
517
576
  if statement_config.parameter_config.supported_execution_parameter_styles and (
518
577
  ParameterStyle.NAMED_PYFORMAT in statement_config.parameter_config.supported_execution_parameter_styles
519
578
  or ParameterStyle.NAMED_COLON in statement_config.parameter_config.supported_execution_parameter_styles
520
579
  ):
521
- 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()}
522
581
  if statement_config.parameter_config.default_parameter_style in {
523
582
  ParameterStyle.NUMERIC,
524
583
  ParameterStyle.QMARK,
525
584
  ParameterStyle.POSITIONAL_PYFORMAT,
526
585
  }:
527
- ordered_parameters = []
528
586
  sorted_items = sorted(
529
587
  parameters.items(),
530
588
  key=lambda item: int(item[0])
531
589
  if item[0].isdigit()
532
590
  else (int(item[0][6:]) if item[0].startswith("param_") and item[0][6:].isdigit() else float("inf")),
533
591
  )
534
- for _, value in sorted_items:
535
- ordered_parameters.append(apply_type_coercion(value))
536
- return ordered_parameters
537
-
538
- return {k: apply_type_coercion(v) for k, v in parameters.items()}
592
+ return [self._apply_coercion(value, statement_config) for _, value in sorted_items]
539
593
 
540
- if isinstance(parameters, (list, tuple)):
541
- coerced_params = [apply_type_coercion(p) for p in parameters]
542
- if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
543
- return tuple(coerced_params)
544
- return coerced_params
594
+ return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
545
595
 
546
- 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
547
600
 
548
601
  def _get_compiled_sql(
549
602
  self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
@@ -625,26 +678,26 @@ class CommonDriverAttributesMixin:
625
678
  )
626
679
 
627
680
  params = statement.parameters
628
- params_key: Any
629
-
630
- def make_hashable(obj: Any) -> Any:
631
- """Recursively convert unhashable types to hashable ones."""
632
- if isinstance(obj, (list, tuple)):
633
- return tuple(make_hashable(item) for item in obj)
634
- if isinstance(obj, dict):
635
- return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
636
- if isinstance(obj, set):
637
- return frozenset(make_hashable(item) for item in obj)
638
- 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
639
692
 
640
693
  try:
641
694
  if isinstance(params, dict):
642
- params_key = make_hashable(params)
695
+ params_key = make_cache_key_hashable(params)
643
696
  elif isinstance(params, (list, tuple)) and params:
644
697
  if isinstance(params[0], dict):
645
- params_key = tuple(make_hashable(d) for d in params)
698
+ params_key = tuple(make_cache_key_hashable(d) for d in params)
646
699
  else:
647
- params_key = make_hashable(params)
700
+ params_key = make_cache_key_hashable(params)
648
701
  elif isinstance(params, (list, tuple)):
649
702
  params_key = ()
650
703
  else:
@@ -655,7 +708,7 @@ class CommonDriverAttributesMixin:
655
708
  base_hash = hash((statement.sql, params_key, statement.is_many, statement.is_script))
656
709
  return f"compiled:{base_hash}:{context_hash}"
657
710
 
658
- def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "Optional[ParameterStyle]":
711
+ def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "ParameterStyle | None":
659
712
  """Determine the dominant parameter style from parameter info list.
660
713
 
661
714
  Args:
@@ -688,7 +741,7 @@ class CommonDriverAttributesMixin:
688
741
  def find_filter(
689
742
  filter_type: "type[FilterTypeT]",
690
743
  filters: "Sequence[StatementFilter | StatementParameters] | Sequence[StatementFilter]",
691
- ) -> "Optional[FilterTypeT]":
744
+ ) -> "FilterTypeT | None":
692
745
  """Get the filter specified by filter type from the filters.
693
746
 
694
747
  Args: