sqlspec 0.32.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.
Files changed (262) hide show
  1. sqlspec/__init__.py +104 -0
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +14 -0
  4. sqlspec/_serialization.py +312 -0
  5. sqlspec/_typing.py +784 -0
  6. sqlspec/adapters/__init__.py +0 -0
  7. sqlspec/adapters/adbc/__init__.py +5 -0
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  10. sqlspec/adapters/adbc/adk/store.py +880 -0
  11. sqlspec/adapters/adbc/config.py +436 -0
  12. sqlspec/adapters/adbc/data_dictionary.py +537 -0
  13. sqlspec/adapters/adbc/driver.py +841 -0
  14. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  15. sqlspec/adapters/adbc/litestar/store.py +504 -0
  16. sqlspec/adapters/adbc/type_converter.py +153 -0
  17. sqlspec/adapters/aiosqlite/__init__.py +29 -0
  18. sqlspec/adapters/aiosqlite/_types.py +13 -0
  19. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  21. sqlspec/adapters/aiosqlite/config.py +310 -0
  22. sqlspec/adapters/aiosqlite/data_dictionary.py +260 -0
  23. sqlspec/adapters/aiosqlite/driver.py +463 -0
  24. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  25. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  26. sqlspec/adapters/aiosqlite/pool.py +500 -0
  27. sqlspec/adapters/asyncmy/__init__.py +25 -0
  28. sqlspec/adapters/asyncmy/_types.py +12 -0
  29. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  31. sqlspec/adapters/asyncmy/config.py +246 -0
  32. sqlspec/adapters/asyncmy/data_dictionary.py +241 -0
  33. sqlspec/adapters/asyncmy/driver.py +632 -0
  34. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  35. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  36. sqlspec/adapters/asyncpg/__init__.py +23 -0
  37. sqlspec/adapters/asyncpg/_type_handlers.py +76 -0
  38. sqlspec/adapters/asyncpg/_types.py +23 -0
  39. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  41. sqlspec/adapters/asyncpg/config.py +464 -0
  42. sqlspec/adapters/asyncpg/data_dictionary.py +321 -0
  43. sqlspec/adapters/asyncpg/driver.py +720 -0
  44. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  45. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  46. sqlspec/adapters/bigquery/__init__.py +18 -0
  47. sqlspec/adapters/bigquery/_types.py +12 -0
  48. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  49. sqlspec/adapters/bigquery/adk/store.py +585 -0
  50. sqlspec/adapters/bigquery/config.py +298 -0
  51. sqlspec/adapters/bigquery/data_dictionary.py +256 -0
  52. sqlspec/adapters/bigquery/driver.py +1073 -0
  53. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  54. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  55. sqlspec/adapters/bigquery/type_converter.py +125 -0
  56. sqlspec/adapters/duckdb/__init__.py +24 -0
  57. sqlspec/adapters/duckdb/_types.py +12 -0
  58. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  59. sqlspec/adapters/duckdb/adk/store.py +563 -0
  60. sqlspec/adapters/duckdb/config.py +396 -0
  61. sqlspec/adapters/duckdb/data_dictionary.py +264 -0
  62. sqlspec/adapters/duckdb/driver.py +604 -0
  63. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  64. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  65. sqlspec/adapters/duckdb/pool.py +273 -0
  66. sqlspec/adapters/duckdb/type_converter.py +133 -0
  67. sqlspec/adapters/oracledb/__init__.py +32 -0
  68. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  69. sqlspec/adapters/oracledb/_types.py +39 -0
  70. sqlspec/adapters/oracledb/_uuid_handlers.py +130 -0
  71. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  72. sqlspec/adapters/oracledb/adk/store.py +1632 -0
  73. sqlspec/adapters/oracledb/config.py +469 -0
  74. sqlspec/adapters/oracledb/data_dictionary.py +717 -0
  75. sqlspec/adapters/oracledb/driver.py +1493 -0
  76. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  77. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  78. sqlspec/adapters/oracledb/migrations.py +532 -0
  79. sqlspec/adapters/oracledb/type_converter.py +207 -0
  80. sqlspec/adapters/psqlpy/__init__.py +16 -0
  81. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  82. sqlspec/adapters/psqlpy/_types.py +12 -0
  83. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  84. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  85. sqlspec/adapters/psqlpy/config.py +271 -0
  86. sqlspec/adapters/psqlpy/data_dictionary.py +179 -0
  87. sqlspec/adapters/psqlpy/driver.py +892 -0
  88. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  90. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  91. sqlspec/adapters/psycopg/__init__.py +32 -0
  92. sqlspec/adapters/psycopg/_type_handlers.py +90 -0
  93. sqlspec/adapters/psycopg/_types.py +18 -0
  94. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  95. sqlspec/adapters/psycopg/adk/store.py +962 -0
  96. sqlspec/adapters/psycopg/config.py +487 -0
  97. sqlspec/adapters/psycopg/data_dictionary.py +630 -0
  98. sqlspec/adapters/psycopg/driver.py +1336 -0
  99. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  100. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  101. sqlspec/adapters/spanner/__init__.py +38 -0
  102. sqlspec/adapters/spanner/_type_handlers.py +186 -0
  103. sqlspec/adapters/spanner/_types.py +12 -0
  104. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  105. sqlspec/adapters/spanner/adk/store.py +435 -0
  106. sqlspec/adapters/spanner/config.py +241 -0
  107. sqlspec/adapters/spanner/data_dictionary.py +95 -0
  108. sqlspec/adapters/spanner/dialect/__init__.py +6 -0
  109. sqlspec/adapters/spanner/dialect/_spangres.py +52 -0
  110. sqlspec/adapters/spanner/dialect/_spanner.py +123 -0
  111. sqlspec/adapters/spanner/driver.py +366 -0
  112. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  113. sqlspec/adapters/spanner/litestar/store.py +266 -0
  114. sqlspec/adapters/spanner/type_converter.py +46 -0
  115. sqlspec/adapters/sqlite/__init__.py +18 -0
  116. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  117. sqlspec/adapters/sqlite/_types.py +11 -0
  118. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  119. sqlspec/adapters/sqlite/adk/store.py +582 -0
  120. sqlspec/adapters/sqlite/config.py +221 -0
  121. sqlspec/adapters/sqlite/data_dictionary.py +256 -0
  122. sqlspec/adapters/sqlite/driver.py +527 -0
  123. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  124. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  125. sqlspec/adapters/sqlite/pool.py +140 -0
  126. sqlspec/base.py +811 -0
  127. sqlspec/builder/__init__.py +146 -0
  128. sqlspec/builder/_base.py +900 -0
  129. sqlspec/builder/_column.py +517 -0
  130. sqlspec/builder/_ddl.py +1642 -0
  131. sqlspec/builder/_delete.py +84 -0
  132. sqlspec/builder/_dml.py +381 -0
  133. sqlspec/builder/_expression_wrappers.py +46 -0
  134. sqlspec/builder/_factory.py +1537 -0
  135. sqlspec/builder/_insert.py +315 -0
  136. sqlspec/builder/_join.py +375 -0
  137. sqlspec/builder/_merge.py +848 -0
  138. sqlspec/builder/_parsing_utils.py +297 -0
  139. sqlspec/builder/_select.py +1615 -0
  140. sqlspec/builder/_update.py +161 -0
  141. sqlspec/builder/_vector_expressions.py +259 -0
  142. sqlspec/cli.py +764 -0
  143. sqlspec/config.py +1540 -0
  144. sqlspec/core/__init__.py +305 -0
  145. sqlspec/core/cache.py +785 -0
  146. sqlspec/core/compiler.py +603 -0
  147. sqlspec/core/filters.py +872 -0
  148. sqlspec/core/hashing.py +274 -0
  149. sqlspec/core/metrics.py +83 -0
  150. sqlspec/core/parameters/__init__.py +64 -0
  151. sqlspec/core/parameters/_alignment.py +266 -0
  152. sqlspec/core/parameters/_converter.py +413 -0
  153. sqlspec/core/parameters/_processor.py +341 -0
  154. sqlspec/core/parameters/_registry.py +201 -0
  155. sqlspec/core/parameters/_transformers.py +226 -0
  156. sqlspec/core/parameters/_types.py +430 -0
  157. sqlspec/core/parameters/_validator.py +123 -0
  158. sqlspec/core/pipeline.py +187 -0
  159. sqlspec/core/result.py +1124 -0
  160. sqlspec/core/splitter.py +940 -0
  161. sqlspec/core/stack.py +163 -0
  162. sqlspec/core/statement.py +835 -0
  163. sqlspec/core/type_conversion.py +235 -0
  164. sqlspec/driver/__init__.py +36 -0
  165. sqlspec/driver/_async.py +1027 -0
  166. sqlspec/driver/_common.py +1236 -0
  167. sqlspec/driver/_sync.py +1025 -0
  168. sqlspec/driver/mixins/__init__.py +7 -0
  169. sqlspec/driver/mixins/_result_tools.py +61 -0
  170. sqlspec/driver/mixins/_sql_translator.py +122 -0
  171. sqlspec/driver/mixins/_storage.py +311 -0
  172. sqlspec/exceptions.py +321 -0
  173. sqlspec/extensions/__init__.py +0 -0
  174. sqlspec/extensions/adk/__init__.py +53 -0
  175. sqlspec/extensions/adk/_types.py +51 -0
  176. sqlspec/extensions/adk/converters.py +172 -0
  177. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  178. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  179. sqlspec/extensions/adk/service.py +181 -0
  180. sqlspec/extensions/adk/store.py +536 -0
  181. sqlspec/extensions/aiosql/__init__.py +10 -0
  182. sqlspec/extensions/aiosql/adapter.py +471 -0
  183. sqlspec/extensions/fastapi/__init__.py +19 -0
  184. sqlspec/extensions/fastapi/extension.py +341 -0
  185. sqlspec/extensions/fastapi/providers.py +543 -0
  186. sqlspec/extensions/flask/__init__.py +36 -0
  187. sqlspec/extensions/flask/_state.py +72 -0
  188. sqlspec/extensions/flask/_utils.py +40 -0
  189. sqlspec/extensions/flask/extension.py +402 -0
  190. sqlspec/extensions/litestar/__init__.py +23 -0
  191. sqlspec/extensions/litestar/_utils.py +52 -0
  192. sqlspec/extensions/litestar/cli.py +92 -0
  193. sqlspec/extensions/litestar/config.py +90 -0
  194. sqlspec/extensions/litestar/handlers.py +316 -0
  195. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  196. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  197. sqlspec/extensions/litestar/plugin.py +638 -0
  198. sqlspec/extensions/litestar/providers.py +454 -0
  199. sqlspec/extensions/litestar/store.py +265 -0
  200. sqlspec/extensions/otel/__init__.py +58 -0
  201. sqlspec/extensions/prometheus/__init__.py +107 -0
  202. sqlspec/extensions/starlette/__init__.py +10 -0
  203. sqlspec/extensions/starlette/_state.py +26 -0
  204. sqlspec/extensions/starlette/_utils.py +52 -0
  205. sqlspec/extensions/starlette/extension.py +257 -0
  206. sqlspec/extensions/starlette/middleware.py +154 -0
  207. sqlspec/loader.py +716 -0
  208. sqlspec/migrations/__init__.py +36 -0
  209. sqlspec/migrations/base.py +728 -0
  210. sqlspec/migrations/commands.py +1140 -0
  211. sqlspec/migrations/context.py +142 -0
  212. sqlspec/migrations/fix.py +203 -0
  213. sqlspec/migrations/loaders.py +450 -0
  214. sqlspec/migrations/runner.py +1024 -0
  215. sqlspec/migrations/templates.py +234 -0
  216. sqlspec/migrations/tracker.py +403 -0
  217. sqlspec/migrations/utils.py +256 -0
  218. sqlspec/migrations/validation.py +203 -0
  219. sqlspec/observability/__init__.py +22 -0
  220. sqlspec/observability/_config.py +228 -0
  221. sqlspec/observability/_diagnostics.py +67 -0
  222. sqlspec/observability/_dispatcher.py +151 -0
  223. sqlspec/observability/_observer.py +180 -0
  224. sqlspec/observability/_runtime.py +381 -0
  225. sqlspec/observability/_spans.py +158 -0
  226. sqlspec/protocols.py +530 -0
  227. sqlspec/py.typed +0 -0
  228. sqlspec/storage/__init__.py +46 -0
  229. sqlspec/storage/_utils.py +104 -0
  230. sqlspec/storage/backends/__init__.py +1 -0
  231. sqlspec/storage/backends/base.py +163 -0
  232. sqlspec/storage/backends/fsspec.py +398 -0
  233. sqlspec/storage/backends/local.py +377 -0
  234. sqlspec/storage/backends/obstore.py +580 -0
  235. sqlspec/storage/errors.py +104 -0
  236. sqlspec/storage/pipeline.py +604 -0
  237. sqlspec/storage/registry.py +289 -0
  238. sqlspec/typing.py +219 -0
  239. sqlspec/utils/__init__.py +31 -0
  240. sqlspec/utils/arrow_helpers.py +95 -0
  241. sqlspec/utils/config_resolver.py +153 -0
  242. sqlspec/utils/correlation.py +132 -0
  243. sqlspec/utils/data_transformation.py +114 -0
  244. sqlspec/utils/dependencies.py +79 -0
  245. sqlspec/utils/deprecation.py +113 -0
  246. sqlspec/utils/fixtures.py +250 -0
  247. sqlspec/utils/logging.py +172 -0
  248. sqlspec/utils/module_loader.py +273 -0
  249. sqlspec/utils/portal.py +325 -0
  250. sqlspec/utils/schema.py +288 -0
  251. sqlspec/utils/serializers.py +396 -0
  252. sqlspec/utils/singleton.py +41 -0
  253. sqlspec/utils/sync_tools.py +277 -0
  254. sqlspec/utils/text.py +108 -0
  255. sqlspec/utils/type_converters.py +99 -0
  256. sqlspec/utils/type_guards.py +1324 -0
  257. sqlspec/utils/version.py +444 -0
  258. sqlspec-0.32.0.dist-info/METADATA +202 -0
  259. sqlspec-0.32.0.dist-info/RECORD +262 -0
  260. sqlspec-0.32.0.dist-info/WHEEL +4 -0
  261. sqlspec-0.32.0.dist-info/entry_points.txt +2 -0
  262. sqlspec-0.32.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,274 @@
1
+ """Statement hashing utilities for cache key generation.
2
+
3
+ Provides hashing functions for SQL statements, expressions, parameters,
4
+ filters, and AST sub-expressions.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from sqlglot import exp
10
+
11
+ from sqlspec.utils.type_guards import is_typed_parameter
12
+
13
+ if TYPE_CHECKING:
14
+ from sqlspec.core.filters import StatementFilter
15
+ from sqlspec.core.statement import SQL
16
+
17
+ __all__ = (
18
+ "hash_expression",
19
+ "hash_expression_node",
20
+ "hash_optimized_expression",
21
+ "hash_parameters",
22
+ "hash_sql_statement",
23
+ )
24
+
25
+
26
+ def hash_expression(expr: exp.Expression | None, _seen: set[int] | None = None) -> int:
27
+ """Generate hash from AST structure.
28
+
29
+ Args:
30
+ expr: SQLGlot Expression to hash
31
+ _seen: Set of seen object IDs to handle circular references
32
+
33
+ Returns:
34
+ Hash of the AST structure
35
+ """
36
+ if expr is None:
37
+ return hash(None)
38
+
39
+ if _seen is None:
40
+ _seen = set()
41
+
42
+ expr_id = id(expr)
43
+ if expr_id in _seen:
44
+ return hash(expr_id)
45
+
46
+ _seen.add(expr_id)
47
+
48
+ components: list[Any] = [type(expr).__name__]
49
+
50
+ for key, value in sorted(expr.args.items()):
51
+ components.extend((key, _hash_value(value, _seen)))
52
+
53
+ return hash(tuple(components))
54
+
55
+
56
+ def _hash_value(value: Any, _seen: set[int]) -> int:
57
+ """Hash different value types.
58
+
59
+ Args:
60
+ value: Value to hash (can be Expression, list, dict, or primitive)
61
+ _seen: Set of seen object IDs to handle circular references
62
+
63
+ Returns:
64
+ Hash of the value
65
+ """
66
+ if isinstance(value, exp.Expression):
67
+ return hash_expression(value, _seen)
68
+ if isinstance(value, list):
69
+ return hash(tuple(_hash_value(v, _seen) for v in value))
70
+ if isinstance(value, dict):
71
+ items = sorted((k, _hash_value(v, _seen)) for k, v in value.items())
72
+ return hash(tuple(items))
73
+ if isinstance(value, tuple):
74
+ return hash(tuple(_hash_value(v, _seen) for v in value))
75
+
76
+ return hash(value)
77
+
78
+
79
+ def hash_parameters(
80
+ positional_parameters: list[Any] | None = None,
81
+ named_parameters: dict[str, Any] | None = None,
82
+ original_parameters: Any | None = None,
83
+ ) -> int:
84
+ """Generate hash for SQL parameters.
85
+
86
+ Args:
87
+ positional_parameters: List of positional parameters
88
+ named_parameters: Dictionary of named parameters
89
+ original_parameters: Original parameters (for execute_many)
90
+
91
+ Returns:
92
+ Combined hash of all parameters
93
+ """
94
+ param_hash = 0
95
+
96
+ if positional_parameters:
97
+ from sqlspec.core.parameters import TypedParameter
98
+
99
+ hashable_parameters: list[tuple[Any, Any]] = []
100
+ for param in positional_parameters:
101
+ if isinstance(param, TypedParameter):
102
+ if isinstance(param.value, (list, dict)):
103
+ hashable_parameters.append((repr(param.value), param.original_type))
104
+ else:
105
+ hashable_parameters.append((param.value, param.original_type))
106
+ elif isinstance(param, (list, dict)):
107
+ hashable_parameters.append((repr(param), "unhashable"))
108
+ else:
109
+ try:
110
+ hash(param)
111
+ hashable_parameters.append((param, "primitive"))
112
+ except TypeError:
113
+ hashable_parameters.append((repr(param), "unhashable_repr"))
114
+
115
+ param_hash ^= hash(tuple(hashable_parameters))
116
+
117
+ if named_parameters:
118
+ hashable_items = []
119
+ for key, value in sorted(named_parameters.items()):
120
+ if is_typed_parameter(value):
121
+ if isinstance(value.value, (list, dict)):
122
+ hashable_items.append((key, (repr(value.value), value.original_type)))
123
+ else:
124
+ hashable_items.append((key, (value.value, value.original_type)))
125
+ elif isinstance(value, (list, dict)):
126
+ hashable_items.append((key, (repr(value), "unhashable")))
127
+ else:
128
+ hashable_items.append((key, (value, "primitive")))
129
+ param_hash ^= hash(tuple(hashable_items))
130
+
131
+ if original_parameters is not None:
132
+ if isinstance(original_parameters, list):
133
+ param_hash ^= hash(("original_count", len(original_parameters)))
134
+ if original_parameters:
135
+ sample_size = min(3, len(original_parameters))
136
+ sample_hash = hash(repr(original_parameters[:sample_size]))
137
+ param_hash ^= hash(("original_sample", sample_hash))
138
+ else:
139
+ param_hash ^= hash(("original", repr(original_parameters)))
140
+
141
+ return param_hash
142
+
143
+
144
+ def _hash_filter_value(value: Any) -> int:
145
+ try:
146
+ return hash(value)
147
+ except TypeError:
148
+ return hash(repr(value))
149
+
150
+
151
+ def hash_filters(filters: list["StatementFilter"] | None = None) -> int:
152
+ """Generate hash for statement filters.
153
+
154
+ Args:
155
+ filters: List of statement filters
156
+
157
+ Returns:
158
+ Hash of the filters
159
+ """
160
+ if not filters:
161
+ return 0
162
+
163
+ filter_components = []
164
+ for f in filters:
165
+ components: list[Any] = [f.__class__.__name__]
166
+
167
+ filter_dict = getattr(f, "__dict__", None)
168
+ if filter_dict is not None:
169
+ for key, value in sorted(filter_dict.items()):
170
+ components.append((key, _hash_filter_value(value)))
171
+
172
+ filter_components.append(tuple(components))
173
+
174
+ return hash(tuple(filter_components))
175
+
176
+
177
+ def hash_sql_statement(statement: "SQL") -> str:
178
+ """Generate cache key for a SQL statement.
179
+
180
+ Args:
181
+ statement: SQL statement object
182
+
183
+ Returns:
184
+ Cache key string
185
+ """
186
+ from sqlspec.utils.type_guards import is_expression
187
+
188
+ stmt_expr = statement.statement_expression
189
+ expr_hash = hash_expression(stmt_expr) if is_expression(stmt_expr) else hash(statement.raw_sql)
190
+
191
+ param_hash = hash_parameters(
192
+ positional_parameters=statement.positional_parameters,
193
+ named_parameters=statement.named_parameters,
194
+ original_parameters=statement.original_parameters,
195
+ )
196
+
197
+ filter_hash = hash_filters(statement.filters)
198
+
199
+ state_components = [
200
+ expr_hash,
201
+ param_hash,
202
+ filter_hash,
203
+ hash(statement.dialect),
204
+ hash(statement.is_many),
205
+ hash(statement.is_script),
206
+ ]
207
+
208
+ return f"sql:{hash(tuple(state_components))}"
209
+
210
+
211
+ def hash_expression_node(node: exp.Expression, include_children: bool = True, dialect: str | None = None) -> str:
212
+ """Generate cache key for an expression node.
213
+
214
+ Args:
215
+ node: The expression node to hash
216
+ include_children: Whether to include child nodes in the hash
217
+ dialect: SQL dialect for context-aware hashing
218
+
219
+ Returns:
220
+ Cache key string for the expression node
221
+ """
222
+ if include_children:
223
+ node_hash = hash_expression(node)
224
+ else:
225
+ components: list[Any] = [type(node).__name__]
226
+ for key, value in sorted(node.args.items()):
227
+ if not isinstance(value, (list, exp.Expression)):
228
+ components.extend((key, hash(value)))
229
+ node_hash = hash(tuple(components))
230
+
231
+ dialect_part = f":{dialect}" if dialect else ""
232
+ return f"expr{dialect_part}:{node_hash}"
233
+
234
+
235
+ def hash_optimized_expression(
236
+ expr: exp.Expression,
237
+ dialect: str,
238
+ schema: dict[str, Any] | None = None,
239
+ optimizer_settings: dict[str, Any] | None = None,
240
+ ) -> str:
241
+ """Generate cache key for optimized expressions.
242
+
243
+ Creates a key that includes expression structure, dialect, schema
244
+ context, and optimizer settings.
245
+
246
+ Args:
247
+ expr: The unoptimized expression
248
+ dialect: Target SQL dialect
249
+ schema: Schema information
250
+ optimizer_settings: Additional optimizer configuration
251
+
252
+ Returns:
253
+ Cache key string for the optimized expression
254
+ """
255
+
256
+ expr_hash = hash_expression(expr)
257
+
258
+ schema_hash = 0
259
+ if schema:
260
+ schema_items = []
261
+ for table_name, table_schema in sorted(schema.items()):
262
+ if isinstance(table_schema, dict):
263
+ schema_items.append((table_name, len(table_schema)))
264
+ else:
265
+ schema_items.append((table_name, hash("unknown")))
266
+ schema_hash = hash(tuple(schema_items))
267
+
268
+ settings_hash = 0
269
+ if optimizer_settings:
270
+ settings_items = sorted(optimizer_settings.items())
271
+ settings_hash = hash(tuple(settings_items))
272
+
273
+ components = (expr_hash, dialect, schema_hash, settings_hash)
274
+ return f"opt:{hash(components)}"
@@ -0,0 +1,83 @@
1
+ """Telemetry helper objects for stack execution."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING: # pragma: no cover - imported for typing only
6
+ from sqlspec.observability import ObservabilityRuntime
7
+
8
+ __all__ = ("StackExecutionMetrics",)
9
+
10
+
11
+ class StackExecutionMetrics:
12
+ """Capture telemetry facts about a stack execution."""
13
+
14
+ __slots__ = (
15
+ "adapter",
16
+ "continue_on_error",
17
+ "duration_s",
18
+ "error_count",
19
+ "error_type",
20
+ "forced_disable",
21
+ "native_pipeline",
22
+ "statement_count",
23
+ )
24
+
25
+ def __init__(
26
+ self,
27
+ adapter: str,
28
+ statement_count: int,
29
+ *,
30
+ continue_on_error: bool,
31
+ native_pipeline: bool,
32
+ forced_disable: bool,
33
+ ) -> None:
34
+ self.adapter = adapter
35
+ self.statement_count = statement_count
36
+ self.continue_on_error = continue_on_error
37
+ self.native_pipeline = native_pipeline
38
+ self.forced_disable = forced_disable
39
+ self.duration_s = 0.0
40
+ self.error_type: str | None = None
41
+ self.error_count = 0
42
+
43
+ def record_duration(self, duration: float) -> None:
44
+ """Record execution duration in seconds."""
45
+
46
+ self.duration_s = duration
47
+
48
+ def record_operation_error(self, error: Exception) -> None:
49
+ """Record an operation error when continue-on-error is enabled."""
50
+
51
+ self.error_count += 1
52
+ if not self.continue_on_error and self.error_type is None:
53
+ self.error_type = type(error).__name__
54
+
55
+ def record_error(self, error: Exception) -> None:
56
+ """Record a terminal error."""
57
+
58
+ self.error_type = type(error).__name__
59
+ self.error_count = max(self.error_count, 1)
60
+
61
+ def emit(self, runtime: "ObservabilityRuntime") -> None:
62
+ """Emit collected metrics to the configured runtime."""
63
+
64
+ runtime.increment_metric("stack.execute.invocations")
65
+ runtime.increment_metric("stack.execute.statements", float(self.statement_count))
66
+
67
+ mode = "continue" if self.continue_on_error else "failfast"
68
+ runtime.increment_metric(f"stack.execute.mode.{mode}")
69
+
70
+ pipeline_label = "native" if self.native_pipeline else "sequential"
71
+ runtime.increment_metric(f"stack.execute.path.{pipeline_label}")
72
+
73
+ if self.forced_disable:
74
+ runtime.increment_metric("stack.execute.override.forced")
75
+
76
+ runtime.increment_metric("stack.execute.duration_ms", self.duration_s * 1000.0)
77
+
78
+ if self.error_type is not None:
79
+ runtime.increment_metric("stack.execute.errors")
80
+ runtime.increment_metric(f"stack.execute.errors.{self.error_type}")
81
+
82
+ if self.error_count and self.continue_on_error:
83
+ runtime.increment_metric("stack.execute.partial_errors", float(self.error_count))
@@ -0,0 +1,64 @@
1
+ """Parameter processing public API."""
2
+
3
+ from sqlspec.core.parameters._alignment import (
4
+ EXECUTE_MANY_MIN_ROWS,
5
+ collect_null_parameter_ordinals,
6
+ looks_like_execute_many,
7
+ normalize_parameter_key,
8
+ validate_parameter_alignment,
9
+ )
10
+ from sqlspec.core.parameters._converter import ParameterConverter
11
+ from sqlspec.core.parameters._processor import ParameterProcessor
12
+ from sqlspec.core.parameters._registry import (
13
+ DRIVER_PARAMETER_PROFILES,
14
+ build_statement_config_from_profile,
15
+ get_driver_profile,
16
+ register_driver_profile,
17
+ )
18
+ from sqlspec.core.parameters._transformers import (
19
+ build_literal_inlining_transform,
20
+ build_null_pruning_transform,
21
+ replace_null_parameters_with_literals,
22
+ replace_placeholders_with_literals,
23
+ )
24
+ from sqlspec.core.parameters._types import (
25
+ DriverParameterProfile,
26
+ ParameterInfo,
27
+ ParameterProcessingResult,
28
+ ParameterProfile,
29
+ ParameterStyle,
30
+ ParameterStyleConfig,
31
+ TypedParameter,
32
+ is_iterable_parameters,
33
+ wrap_with_type,
34
+ )
35
+ from sqlspec.core.parameters._validator import PARAMETER_REGEX, ParameterValidator
36
+
37
+ __all__ = (
38
+ "DRIVER_PARAMETER_PROFILES",
39
+ "EXECUTE_MANY_MIN_ROWS",
40
+ "PARAMETER_REGEX",
41
+ "DriverParameterProfile",
42
+ "ParameterConverter",
43
+ "ParameterInfo",
44
+ "ParameterProcessingResult",
45
+ "ParameterProcessor",
46
+ "ParameterProfile",
47
+ "ParameterStyle",
48
+ "ParameterStyleConfig",
49
+ "ParameterValidator",
50
+ "TypedParameter",
51
+ "build_literal_inlining_transform",
52
+ "build_null_pruning_transform",
53
+ "build_statement_config_from_profile",
54
+ "collect_null_parameter_ordinals",
55
+ "get_driver_profile",
56
+ "is_iterable_parameters",
57
+ "looks_like_execute_many",
58
+ "normalize_parameter_key",
59
+ "register_driver_profile",
60
+ "replace_null_parameters_with_literals",
61
+ "replace_placeholders_with_literals",
62
+ "validate_parameter_alignment",
63
+ "wrap_with_type",
64
+ )
@@ -0,0 +1,266 @@
1
+ """Parameter alignment and validation helpers."""
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, cast
5
+
6
+ import sqlspec.exceptions
7
+ from sqlspec.core.parameters._types import ParameterProfile, ParameterStyle
8
+
9
+ __all__ = (
10
+ "EXECUTE_MANY_MIN_ROWS",
11
+ "collect_null_parameter_ordinals",
12
+ "looks_like_execute_many",
13
+ "normalize_parameter_key",
14
+ "validate_parameter_alignment",
15
+ )
16
+
17
+ EXECUTE_MANY_MIN_ROWS: int = 2
18
+
19
+
20
+ def _is_sequence_like(value: Any) -> bool:
21
+ return isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray))
22
+
23
+
24
+ def normalize_parameter_key(key: Any) -> "tuple[str, int | str]":
25
+ """Normalize a parameter key into an ``(kind, value)`` tuple.
26
+
27
+ Args:
28
+ key: Key supplied by the caller (index, name, or adapter-specific token).
29
+
30
+ Returns:
31
+ Tuple identifying the key type and canonical value for alignment checks.
32
+ """
33
+ if isinstance(key, str):
34
+ stripped_numeric = key.lstrip("$")
35
+ if stripped_numeric.isdigit():
36
+ return ("index", int(stripped_numeric) - 1)
37
+ if key.isdigit():
38
+ return ("index", int(key) - 1)
39
+ return ("named", key)
40
+ if isinstance(key, int):
41
+ if key > 0:
42
+ return ("index", key - 1)
43
+ return ("index", key)
44
+ return ("named", str(key))
45
+
46
+
47
+ def looks_like_execute_many(parameters: Any) -> bool:
48
+ """Return ``True`` when the payload resembles an ``execute_many`` structure.
49
+
50
+ Args:
51
+ parameters: Potential parameter payload to inspect.
52
+
53
+ Returns:
54
+ ``True`` if the payload appears to be a sequence of parameter sets.
55
+ """
56
+ if not _is_sequence_like(parameters) or len(parameters) < EXECUTE_MANY_MIN_ROWS:
57
+ return False
58
+ return all(_is_sequence_like(entry) or isinstance(entry, Mapping) for entry in parameters)
59
+
60
+
61
+ def collect_null_parameter_ordinals(parameters: Any, profile: "ParameterProfile") -> "set[int]":
62
+ """Identify placeholder ordinals whose provided values are ``None``.
63
+
64
+ Args:
65
+ parameters: Parameter payload supplied by the caller.
66
+ profile: Metadata describing detected placeholders.
67
+
68
+ Returns:
69
+ Set of ordinal indices corresponding to ``None`` values.
70
+ """
71
+ if parameters is None:
72
+ return set()
73
+
74
+ null_positions: set[int] = set()
75
+
76
+ if isinstance(parameters, Mapping):
77
+ name_lookup: dict[str, int] = {}
78
+ for parameter in profile.parameters:
79
+ if parameter.name:
80
+ name_lookup[parameter.name] = parameter.ordinal
81
+ stripped_name = parameter.name.lstrip("@")
82
+ name_lookup.setdefault(stripped_name, parameter.ordinal)
83
+ name_lookup.setdefault(f"@{stripped_name}", parameter.ordinal)
84
+
85
+ for key, value in parameters.items():
86
+ if value is not None:
87
+ continue
88
+ key_kind, normalized_key = normalize_parameter_key(key)
89
+ if key_kind == "index" and isinstance(normalized_key, int):
90
+ null_positions.add(normalized_key)
91
+ continue
92
+ if key_kind == "named":
93
+ ordinal = name_lookup.get(str(normalized_key))
94
+ if ordinal is not None:
95
+ null_positions.add(ordinal)
96
+ return null_positions
97
+
98
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, bytearray)):
99
+ for index, value in enumerate(parameters):
100
+ if value is None:
101
+ null_positions.add(index)
102
+ return null_positions
103
+
104
+ return null_positions
105
+
106
+
107
+ def _collect_expected_identifiers(parameter_profile: "ParameterProfile") -> "set[tuple[str, int | str]]":
108
+ identifiers: set[tuple[str, int | str]] = set()
109
+ for parameter in parameter_profile.parameters:
110
+ style = parameter.style
111
+ name = parameter.name
112
+ if style in {
113
+ ParameterStyle.NAMED_COLON,
114
+ ParameterStyle.NAMED_AT,
115
+ ParameterStyle.NAMED_DOLLAR,
116
+ ParameterStyle.NAMED_PYFORMAT,
117
+ }:
118
+ identifiers.add(("named", name or f"param_{parameter.ordinal}"))
119
+ elif style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON}:
120
+ if name and name.isdigit():
121
+ identifiers.add(("index", int(name) - 1))
122
+ else:
123
+ identifiers.add(("index", parameter.ordinal))
124
+ else:
125
+ identifiers.add(("index", parameter.ordinal))
126
+ return identifiers
127
+
128
+
129
+ def _collect_actual_identifiers(parameters: Any) -> "tuple[set[tuple[str, int | str]], int]":
130
+ if parameters is None:
131
+ return set(), 0
132
+ if isinstance(parameters, Mapping):
133
+ mapping_identifiers = {normalize_parameter_key(key) for key in parameters}
134
+ return mapping_identifiers, len(parameters)
135
+ if looks_like_execute_many(parameters):
136
+ aggregated_identifiers: set[tuple[str, int | str]] = set()
137
+ for entry in parameters:
138
+ entry_identifiers, _ = _collect_actual_identifiers(entry)
139
+ aggregated_identifiers.update(entry_identifiers)
140
+ return aggregated_identifiers, len(aggregated_identifiers)
141
+ if _is_sequence_like(parameters):
142
+ identifiers = {("index", cast("int | str", index)) for index in range(len(parameters))}
143
+ return identifiers, len(parameters)
144
+ identifiers = {("index", cast("int | str", 0))}
145
+ return identifiers, 1
146
+
147
+
148
+ def _format_identifiers(identifiers: "set[tuple[str, int | str]]") -> str:
149
+ if not identifiers:
150
+ return "[]"
151
+ formatted: list[str] = []
152
+ for identifier in sorted(identifiers, key=lambda item: (item[0], str(item[1]))):
153
+ kind, value = identifier
154
+ if kind == "named":
155
+ formatted.append(str(value))
156
+ elif isinstance(value, int):
157
+ formatted.append(str(value + 1))
158
+ else:
159
+ formatted.append(str(value))
160
+ return "[" + ", ".join(formatted) + "]"
161
+
162
+
163
+ def _normalize_index_identifiers(expected: "set[tuple[str, int | str]]", actual: "set[tuple[str, int | str]]") -> bool:
164
+ """Allow positional payloads to satisfy generated param_N identifiers."""
165
+
166
+ if not expected or not actual:
167
+ return False
168
+
169
+ expected_named = {value for kind, value in expected if kind == "named"}
170
+ actual_indexes = {value for kind, value in actual if kind == "index"}
171
+
172
+ if not expected_named or not actual_indexes:
173
+ return False
174
+
175
+ normalized_expected: set[int] = set()
176
+ for name in expected_named:
177
+ if not isinstance(name, str) or not name.startswith("param_"):
178
+ return False
179
+ suffix = name[6:]
180
+ if not suffix.isdigit():
181
+ return False
182
+ normalized_expected.add(int(suffix))
183
+
184
+ if not normalized_expected:
185
+ return False
186
+
187
+ if not all(isinstance(index, int) for index in actual_indexes):
188
+ return False
189
+
190
+ normalized_actual = {int(index) for index in actual_indexes}
191
+ return normalized_actual == normalized_expected
192
+
193
+
194
+ def _validate_single_parameter_set(
195
+ parameter_profile: "ParameterProfile", parameters: Any, batch_index: "int | None" = None
196
+ ) -> None:
197
+ expected_identifiers = _collect_expected_identifiers(parameter_profile)
198
+ actual_identifiers, actual_count = _collect_actual_identifiers(parameters)
199
+ expected_count = len(expected_identifiers)
200
+
201
+ if expected_count == 0 and actual_count == 0:
202
+ return
203
+
204
+ prefix = "Parameter count mismatch"
205
+ if batch_index is not None:
206
+ prefix = f"{prefix} in batch {batch_index}"
207
+
208
+ if expected_count == 0 and actual_count > 0:
209
+ msg = f"{prefix}: statement does not accept parameters."
210
+ raise sqlspec.exceptions.SQLSpecError(msg)
211
+
212
+ if expected_count > 0 and actual_count == 0:
213
+ msg = f"{prefix}: expected {expected_count} parameters, received 0."
214
+ raise sqlspec.exceptions.SQLSpecError(msg)
215
+
216
+ if expected_count != actual_count:
217
+ msg = f"{prefix}: {actual_count} parameters provided but {expected_count} placeholders detected."
218
+ raise sqlspec.exceptions.SQLSpecError(msg)
219
+
220
+ identifiers_match = expected_identifiers == actual_identifiers or _normalize_index_identifiers(
221
+ expected_identifiers, actual_identifiers
222
+ )
223
+
224
+ if not identifiers_match:
225
+ msg = (
226
+ f"{prefix}: expected identifiers {_format_identifiers(expected_identifiers)}, "
227
+ f"received {_format_identifiers(actual_identifiers)}."
228
+ )
229
+ raise sqlspec.exceptions.SQLSpecError(msg)
230
+
231
+
232
+ def validate_parameter_alignment(
233
+ parameter_profile: "ParameterProfile | None", parameters: Any, *, is_many: bool = False
234
+ ) -> None:
235
+ """Ensure provided parameters align with detected placeholders.
236
+
237
+ Args:
238
+ parameter_profile: Placeholder metadata extracted from the statement.
239
+ parameters: Parameter payload the adapter will execute with.
240
+ is_many: Whether the call explicitly targets ``execute_many``.
241
+
242
+ Raises:
243
+ SQLSpecError: If counts or identifiers differ between placeholders and payload.
244
+ """
245
+ profile = parameter_profile or ParameterProfile.empty()
246
+ if profile.total_count == 0:
247
+ return
248
+
249
+ effective_is_many = is_many or looks_like_execute_many(parameters)
250
+
251
+ if effective_is_many:
252
+ if parameters is None:
253
+ if profile.total_count == 0:
254
+ return
255
+ msg = "Parameter count mismatch: expected parameter sets for execute_many."
256
+ raise sqlspec.exceptions.SQLSpecError(msg)
257
+ if not _is_sequence_like(parameters):
258
+ msg = "Parameter count mismatch: expected sequence of parameter sets for execute_many."
259
+ raise sqlspec.exceptions.SQLSpecError(msg)
260
+ if len(parameters) == 0:
261
+ return
262
+ for index, param_set in enumerate(parameters):
263
+ _validate_single_parameter_set(profile, param_set, batch_index=index)
264
+ return
265
+
266
+ _validate_single_parameter_set(profile, parameters)