sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -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 +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 +536 -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 +503 -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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -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 +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -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 +582 -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 +331 -62
  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 +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  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 +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.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,8 +11,9 @@ 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
+ from sqlspec.utils.type_guards import is_statement_filter
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from collections.abc import Sequence
@@ -31,6 +32,8 @@ __all__ = (
31
32
  "ExecutionResult",
32
33
  "ScriptExecutionResult",
33
34
  "VersionInfo",
35
+ "handle_single_row_error",
36
+ "make_cache_key_hashable",
34
37
  )
35
38
 
36
39
 
@@ -41,6 +44,69 @@ VERSION_GROUPS_MIN_FOR_MINOR = 1
41
44
  VERSION_GROUPS_MIN_FOR_PATCH = 2
42
45
 
43
46
 
47
+ def make_cache_key_hashable(obj: Any) -> Any:
48
+ """Recursively convert unhashable types to hashable ones for cache keys.
49
+
50
+ For array-like objects (NumPy arrays, Python arrays, etc.), we use structural
51
+ info (dtype + shape or typecode + length) rather than content for cache keys.
52
+ This ensures high cache hit rates for parameterized queries with different
53
+ vector values while avoiding expensive content hashing.
54
+
55
+ Args:
56
+ obj: Object to make hashable.
57
+
58
+ Returns:
59
+ A hashable representation of the object. Collections become tuples,
60
+ arrays become structural tuples like ("ndarray", dtype, shape).
61
+
62
+ Examples:
63
+ >>> make_cache_key_hashable([1, 2, 3])
64
+ (1, 2, 3)
65
+ >>> make_cache_key_hashable({"a": 1, "b": 2})
66
+ (('a', 1), ('b', 2))
67
+ """
68
+ if isinstance(obj, (list, tuple)):
69
+ return tuple(make_cache_key_hashable(item) for item in obj)
70
+ if isinstance(obj, dict):
71
+ return tuple(sorted((k, make_cache_key_hashable(v)) for k, v in obj.items()))
72
+ if isinstance(obj, set):
73
+ return frozenset(make_cache_key_hashable(item) for item in obj)
74
+
75
+ typecode = getattr(obj, "typecode", None)
76
+ if typecode is not None:
77
+ try:
78
+ length = len(obj)
79
+ except (AttributeError, TypeError):
80
+ return ("array", typecode)
81
+ else:
82
+ return ("array", typecode, length)
83
+
84
+ if hasattr(obj, "__array__"):
85
+ try:
86
+ dtype_str = getattr(obj.dtype, "str", str(type(obj)))
87
+ shape = tuple(int(s) for s in obj.shape)
88
+ except (AttributeError, TypeError):
89
+ try:
90
+ length = len(obj)
91
+ except (AttributeError, TypeError):
92
+ return ("array_like", type(obj).__name__)
93
+ else:
94
+ return ("array_like", type(obj).__name__, length)
95
+ else:
96
+ return ("ndarray", dtype_str, shape)
97
+ return obj
98
+
99
+
100
+ def handle_single_row_error(error: ValueError) -> "NoReturn":
101
+ """Normalize single-row selection errors to SQLSpec exceptions."""
102
+
103
+ message = str(error)
104
+ if message.startswith("No result found"):
105
+ msg = "No rows found"
106
+ raise NotFoundError(msg) from error
107
+ raise error
108
+
109
+
44
110
  class VersionInfo:
45
111
  """Database version information."""
46
112
 
@@ -100,7 +166,7 @@ class VersionInfo:
100
166
  class DataDictionaryMixin:
101
167
  """Mixin providing common data dictionary functionality."""
102
168
 
103
- def parse_version_string(self, version_str: str) -> "Optional[VersionInfo]":
169
+ def parse_version_string(self, version_str: str) -> "VersionInfo | None":
104
170
  """Parse version string into VersionInfo.
105
171
 
106
172
  Args:
@@ -128,7 +194,7 @@ class DataDictionaryMixin:
128
194
 
129
195
  return None
130
196
 
131
- def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "Optional[VersionInfo]":
197
+ def detect_version_with_queries(self, driver: Any, queries: "list[str]") -> "VersionInfo | None":
132
198
  """Try multiple version queries to detect database version.
133
199
 
134
200
  Args:
@@ -184,7 +250,7 @@ class ScriptExecutionResult(NamedTuple):
184
250
  """Result from script execution with statement count information."""
185
251
 
186
252
  cursor_result: Any
187
- rowcount_override: Optional[int]
253
+ rowcount_override: int | None
188
254
  special_data: Any
189
255
  statement_count: int
190
256
  successful_statements: int
@@ -194,23 +260,23 @@ class ExecutionResult(NamedTuple):
194
260
  """Execution result containing all data needed for SQLResult building."""
195
261
 
196
262
  cursor_result: Any
197
- rowcount_override: Optional[int]
263
+ rowcount_override: int | None
198
264
  special_data: Any
199
265
  selected_data: Optional["list[dict[str, Any]]"]
200
266
  column_names: Optional["list[str]"]
201
- data_row_count: Optional[int]
202
- statement_count: Optional[int]
203
- successful_statements: Optional[int]
267
+ data_row_count: int | None
268
+ statement_count: int | None
269
+ successful_statements: int | None
204
270
  is_script_result: bool
205
271
  is_select_result: bool
206
272
  is_many_result: bool
207
- last_inserted_id: Optional[Union[int, str]] = None
273
+ last_inserted_id: int | str | None = None
208
274
 
209
275
 
210
276
  EXEC_CURSOR_RESULT: Final[int] = 0
211
277
  EXEC_ROWCOUNT_OVERRIDE: Final[int] = 1
212
278
  EXEC_SPECIAL_DATA: Final[int] = 2
213
- DEFAULT_EXECUTION_RESULT: Final[tuple[Any, Optional[int], Any]] = (None, None, None)
279
+ DEFAULT_EXECUTION_RESULT: Final[tuple[Any, int | None, Any]] = (None, None, None)
214
280
 
215
281
 
216
282
  @trait
@@ -223,7 +289,7 @@ class CommonDriverAttributesMixin:
223
289
  driver_features: "dict[str, Any]"
224
290
 
225
291
  def __init__(
226
- self, connection: "Any", statement_config: "StatementConfig", driver_features: "Optional[dict[str, Any]]" = None
292
+ self, connection: "Any", statement_config: "StatementConfig", driver_features: "dict[str, Any] | None" = None
227
293
  ) -> None:
228
294
  """Initialize driver adapter with connection and configuration.
229
295
 
@@ -240,17 +306,17 @@ class CommonDriverAttributesMixin:
240
306
  self,
241
307
  cursor_result: Any,
242
308
  *,
243
- rowcount_override: Optional[int] = None,
309
+ rowcount_override: int | None = None,
244
310
  special_data: Any = None,
245
311
  selected_data: Optional["list[dict[str, Any]]"] = None,
246
312
  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,
313
+ data_row_count: int | None = None,
314
+ statement_count: int | None = None,
315
+ successful_statements: int | None = None,
250
316
  is_script_result: bool = False,
251
317
  is_select_result: bool = False,
252
318
  is_many_result: bool = False,
253
- last_inserted_id: Optional[Union[int, str]] = None,
319
+ last_inserted_id: int | str | None = None,
254
320
  ) -> ExecutionResult:
255
321
  """Create ExecutionResult with all necessary data for any operation type.
256
322
 
@@ -328,11 +394,11 @@ class CommonDriverAttributesMixin:
328
394
 
329
395
  def prepare_statement(
330
396
  self,
331
- statement: "Union[Statement, QueryBuilder]",
332
- parameters: "tuple[Union[StatementParameters, StatementFilter], ...]" = (),
397
+ statement: "Statement | QueryBuilder",
398
+ parameters: "tuple[StatementParameters | StatementFilter, ...]" = (),
333
399
  *,
334
400
  statement_config: "StatementConfig",
335
- kwargs: "Optional[dict[str, Any]]" = None,
401
+ kwargs: "dict[str, Any] | None" = None,
336
402
  ) -> "SQL":
337
403
  """Build SQL statement from various input types.
338
404
 
@@ -349,48 +415,82 @@ class CommonDriverAttributesMixin:
349
415
  """
350
416
  kwargs = kwargs or {}
351
417
 
418
+ filters: list[StatementFilter] = []
419
+ data_parameters: list[StatementParameters] = []
420
+
421
+ for param in parameters:
422
+ if is_statement_filter(param):
423
+ filters.append(param)
424
+ else:
425
+ data_parameters.append(param)
426
+
352
427
  if isinstance(statement, QueryBuilder):
353
428
  sql_statement = statement.to_statement(statement_config)
354
- if parameters or kwargs:
429
+ if data_parameters or kwargs:
355
430
  merged_parameters = (
356
- (*sql_statement.positional_parameters, *parameters)
357
- if parameters
431
+ (*sql_statement.positional_parameters, *tuple(data_parameters))
432
+ if data_parameters
358
433
  else sql_statement.positional_parameters
359
434
  )
360
- return SQL(sql_statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
435
+ sql_statement = SQL(sql_statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
436
+
437
+ for filter_obj in filters:
438
+ sql_statement = filter_obj.append_to_statement(sql_statement)
439
+
361
440
  return sql_statement
441
+
362
442
  if isinstance(statement, SQL):
363
- if parameters or kwargs:
443
+ sql_statement = statement
444
+
445
+ if data_parameters or kwargs:
364
446
  merged_parameters = (
365
- (*statement.positional_parameters, *parameters) if parameters else statement.positional_parameters
447
+ (*sql_statement.positional_parameters, *tuple(data_parameters))
448
+ if data_parameters
449
+ else sql_statement.positional_parameters
366
450
  )
367
- return SQL(statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
368
- needs_rebuild = False
369
-
370
- if statement_config.dialect and (
371
- not statement.statement_config.dialect or statement.statement_config.dialect != statement_config.dialect
372
- ):
373
- needs_rebuild = True
451
+ sql_statement = SQL(sql_statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
452
+ else:
453
+ needs_rebuild = False
454
+
455
+ if statement_config.dialect and (
456
+ not sql_statement.statement_config.dialect
457
+ or sql_statement.statement_config.dialect != statement_config.dialect
458
+ ):
459
+ needs_rebuild = True
460
+
461
+ if (
462
+ sql_statement.statement_config.parameter_config.default_execution_parameter_style
463
+ != statement_config.parameter_config.default_execution_parameter_style
464
+ ):
465
+ needs_rebuild = True
466
+
467
+ if needs_rebuild:
468
+ sql_text = sql_statement.raw_sql or sql_statement.sql
469
+
470
+ if sql_statement.is_many and sql_statement.parameters:
471
+ sql_statement = SQL(
472
+ sql_text, sql_statement.parameters, statement_config=statement_config, is_many=True
473
+ )
474
+ elif sql_statement.named_parameters:
475
+ sql_statement = SQL(
476
+ sql_text, statement_config=statement_config, **sql_statement.named_parameters
477
+ )
478
+ else:
479
+ sql_statement = SQL(
480
+ sql_text, *sql_statement.positional_parameters, statement_config=statement_config
481
+ )
482
+
483
+ for filter_obj in filters:
484
+ sql_statement = filter_obj.append_to_statement(sql_statement)
374
485
 
375
- if (
376
- statement.statement_config.parameter_config.default_execution_parameter_style
377
- != statement_config.parameter_config.default_execution_parameter_style
378
- ):
379
- needs_rebuild = True
486
+ return sql_statement
380
487
 
381
- if needs_rebuild:
382
- sql_text = statement.raw_sql or statement.sql
488
+ sql_statement = SQL(statement, *tuple(data_parameters), statement_config=statement_config, **kwargs)
383
489
 
384
- if statement.is_many and statement.parameters:
385
- new_sql = SQL(sql_text, statement.parameters, statement_config=statement_config, is_many=True)
386
- elif statement.named_parameters:
387
- new_sql = SQL(sql_text, statement_config=statement_config, **statement.named_parameters)
388
- else:
389
- new_sql = SQL(sql_text, *statement.positional_parameters, statement_config=statement_config)
490
+ for filter_obj in filters:
491
+ sql_statement = filter_obj.append_to_statement(sql_statement)
390
492
 
391
- return new_sql
392
- return statement
393
- return SQL(statement, *parameters, statement_config=statement_config, **kwargs)
493
+ return sql_statement
394
494
 
395
495
  def split_script_statements(
396
496
  self, script: str, statement_config: "StatementConfig", strip_trailing_semicolon: bool = False
@@ -421,7 +521,7 @@ class CommonDriverAttributesMixin:
421
521
  parameters: Any,
422
522
  statement_config: "StatementConfig",
423
523
  is_many: bool = False,
424
- prepared_statement: Optional[Any] = None, # pyright: ignore[reportUnusedParameter]
524
+ prepared_statement: Any | None = None, # pyright: ignore[reportUnusedParameter]
425
525
  ) -> Any:
426
526
  """Prepare parameters for database driver consumption.
427
527
 
@@ -449,6 +549,23 @@ class CommonDriverAttributesMixin:
449
549
  return [self._format_parameter_set_for_many(parameters, statement_config)]
450
550
  return self._format_parameter_set(parameters, statement_config)
451
551
 
552
+ def _apply_coercion(self, value: Any, statement_config: "StatementConfig") -> Any:
553
+ """Apply type coercion to a single value.
554
+
555
+ Args:
556
+ value: Value to coerce (may be TypedParameter or raw value)
557
+ statement_config: Statement configuration for type coercion map
558
+
559
+ Returns:
560
+ Coerced value with TypedParameter unwrapped
561
+ """
562
+ unwrapped_value = value.value if isinstance(value, TypedParameter) else value
563
+ if statement_config.parameter_config.type_coercion_map:
564
+ for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
565
+ if isinstance(unwrapped_value, type_check):
566
+ return converter(unwrapped_value)
567
+ return unwrapped_value
568
+
452
569
  def _format_parameter_set_for_many(self, parameters: Any, statement_config: "StatementConfig") -> Any:
453
570
  """Prepare a single parameter set for execute_many operations.
454
571
 
@@ -465,27 +582,14 @@ class CommonDriverAttributesMixin:
465
582
  if not parameters:
466
583
  return []
467
584
 
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
585
+ if not isinstance(parameters, (dict, list, tuple)):
586
+ return self._apply_coercion(parameters, statement_config)
480
587
 
481
588
  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
589
+ return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
487
590
 
488
- return apply_type_coercion(parameters)
591
+ coerced_params = [self._apply_coercion(p, statement_config) for p in parameters]
592
+ return tuple(coerced_params) if isinstance(parameters, tuple) else coerced_params
489
593
 
490
594
  def _format_parameter_set(self, parameters: Any, statement_config: "StatementConfig") -> Any:
491
595
  """Prepare a single parameter set for database driver consumption.
@@ -500,50 +604,34 @@ class CommonDriverAttributesMixin:
500
604
  if not parameters:
501
605
  return []
502
606
 
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
607
+ if not isinstance(parameters, (dict, list, tuple)):
608
+ return [self._apply_coercion(parameters, statement_config)]
513
609
 
514
610
  if isinstance(parameters, dict):
515
- if not parameters:
516
- return []
517
611
  if statement_config.parameter_config.supported_execution_parameter_styles and (
518
612
  ParameterStyle.NAMED_PYFORMAT in statement_config.parameter_config.supported_execution_parameter_styles
519
613
  or ParameterStyle.NAMED_COLON in statement_config.parameter_config.supported_execution_parameter_styles
520
614
  ):
521
- return {k: apply_type_coercion(v) for k, v in parameters.items()}
615
+ return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
522
616
  if statement_config.parameter_config.default_parameter_style in {
523
617
  ParameterStyle.NUMERIC,
524
618
  ParameterStyle.QMARK,
525
619
  ParameterStyle.POSITIONAL_PYFORMAT,
526
620
  }:
527
- ordered_parameters = []
528
621
  sorted_items = sorted(
529
622
  parameters.items(),
530
623
  key=lambda item: int(item[0])
531
624
  if item[0].isdigit()
532
625
  else (int(item[0][6:]) if item[0].startswith("param_") and item[0][6:].isdigit() else float("inf")),
533
626
  )
534
- for _, value in sorted_items:
535
- ordered_parameters.append(apply_type_coercion(value))
536
- return ordered_parameters
627
+ return [self._apply_coercion(value, statement_config) for _, value in sorted_items]
537
628
 
538
- return {k: apply_type_coercion(v) for k, v in parameters.items()}
629
+ return {k: self._apply_coercion(v, statement_config) for k, v in parameters.items()}
539
630
 
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
545
-
546
- return [apply_type_coercion(parameters)]
631
+ coerced_params = [self._apply_coercion(p, statement_config) for p in parameters]
632
+ if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
633
+ return tuple(coerced_params)
634
+ return coerced_params
547
635
 
548
636
  def _get_compiled_sql(
549
637
  self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
@@ -625,26 +713,26 @@ class CommonDriverAttributesMixin:
625
713
  )
626
714
 
627
715
  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
716
+
717
+ if params is None or (isinstance(params, (list, tuple, dict)) and not params):
718
+ return f"compiled:{hash(statement.sql)}:{context_hash}"
719
+
720
+ if isinstance(params, tuple) and all(isinstance(p, (int, str, bytes, bool, type(None))) for p in params):
721
+ try:
722
+ return (
723
+ f"compiled:{hash((statement.sql, params, statement.is_many, statement.is_script))}:{context_hash}"
724
+ )
725
+ except TypeError:
726
+ pass
639
727
 
640
728
  try:
641
729
  if isinstance(params, dict):
642
- params_key = make_hashable(params)
730
+ params_key = make_cache_key_hashable(params)
643
731
  elif isinstance(params, (list, tuple)) and params:
644
732
  if isinstance(params[0], dict):
645
- params_key = tuple(make_hashable(d) for d in params)
733
+ params_key = tuple(make_cache_key_hashable(d) for d in params)
646
734
  else:
647
- params_key = make_hashable(params)
735
+ params_key = make_cache_key_hashable(params)
648
736
  elif isinstance(params, (list, tuple)):
649
737
  params_key = ()
650
738
  else:
@@ -655,7 +743,7 @@ class CommonDriverAttributesMixin:
655
743
  base_hash = hash((statement.sql, params_key, statement.is_many, statement.is_script))
656
744
  return f"compiled:{base_hash}:{context_hash}"
657
745
 
658
- def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "Optional[ParameterStyle]":
746
+ def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "ParameterStyle | None":
659
747
  """Determine the dominant parameter style from parameter info list.
660
748
 
661
749
  Args:
@@ -688,7 +776,7 @@ class CommonDriverAttributesMixin:
688
776
  def find_filter(
689
777
  filter_type: "type[FilterTypeT]",
690
778
  filters: "Sequence[StatementFilter | StatementParameters] | Sequence[StatementFilter]",
691
- ) -> "Optional[FilterTypeT]":
779
+ ) -> "FilterTypeT | None":
692
780
  """Get the filter specified by filter type from the filters.
693
781
 
694
782
  Args: