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,226 @@
1
+ """AST transformer helpers for parameter processing."""
2
+
3
+ import bisect
4
+ from collections.abc import Callable, Mapping, Sequence
5
+ from typing import Any
6
+
7
+ from sqlspec.core.parameters._alignment import (
8
+ collect_null_parameter_ordinals,
9
+ looks_like_execute_many,
10
+ normalize_parameter_key,
11
+ validate_parameter_alignment,
12
+ )
13
+ from sqlspec.core.parameters._types import ParameterProfile
14
+ from sqlspec.core.parameters._validator import ParameterValidator
15
+
16
+ __all__ = (
17
+ "build_literal_inlining_transform",
18
+ "build_null_pruning_transform",
19
+ "replace_null_parameters_with_literals",
20
+ "replace_placeholders_with_literals",
21
+ )
22
+
23
+ _AST_TRANSFORMER_VALIDATOR: "ParameterValidator" = ParameterValidator()
24
+
25
+
26
+ def build_null_pruning_transform(
27
+ *, dialect: str = "postgres", validator: "ParameterValidator | None" = None
28
+ ) -> "Callable[[Any, Any], tuple[Any, Any]]":
29
+ """Return a callable that prunes NULL placeholders from an expression."""
30
+
31
+ def transform(expression: Any, parameters: Any) -> "tuple[Any, Any]":
32
+ return replace_null_parameters_with_literals(expression, parameters, dialect=dialect, validator=validator)
33
+
34
+ return transform
35
+
36
+
37
+ def build_literal_inlining_transform(
38
+ *, json_serializer: "Callable[[Any], str]"
39
+ ) -> "Callable[[Any, Any], tuple[Any, Any]]":
40
+ """Return a callable that replaces placeholders with SQL literals."""
41
+
42
+ def transform(expression: Any, parameters: Any) -> "tuple[Any, Any]":
43
+ literal_expression = replace_placeholders_with_literals(expression, parameters, json_serializer=json_serializer)
44
+ return literal_expression, parameters
45
+
46
+ return transform
47
+
48
+
49
+ def replace_null_parameters_with_literals(
50
+ expression: Any, parameters: Any, *, dialect: str = "postgres", validator: "ParameterValidator | None" = None
51
+ ) -> "tuple[Any, Any]":
52
+ """Rewrite placeholders representing ``NULL`` values and prune parameters.
53
+
54
+ Args:
55
+ expression: SQLGlot expression tree to transform.
56
+ parameters: Parameter payload provided by the caller.
57
+ dialect: SQLGlot dialect for serializing the expression.
58
+ validator: Optional validator instance for parameter extraction.
59
+
60
+ Returns:
61
+ Tuple containing the transformed expression and updated parameters.
62
+ """
63
+ if not parameters:
64
+ return expression, parameters
65
+
66
+ if looks_like_execute_many(parameters):
67
+ return expression, parameters
68
+
69
+ validator_instance = validator or _AST_TRANSFORMER_VALIDATOR
70
+ parameter_info = validator_instance.extract_parameters(expression.sql(dialect=dialect))
71
+ parameter_profile = ParameterProfile(parameter_info)
72
+ validate_parameter_alignment(parameter_profile, parameters)
73
+
74
+ null_positions = collect_null_parameter_ordinals(parameters, parameter_profile)
75
+ if not null_positions:
76
+ return expression, parameters
77
+
78
+ sorted_null_positions = sorted(null_positions)
79
+
80
+ from sqlglot import exp as _exp # Imported lazily to avoid module-level dependency
81
+
82
+ qmark_position = 0
83
+
84
+ def transform_node(node: Any) -> Any:
85
+ nonlocal qmark_position
86
+
87
+ if isinstance(node, _exp.Placeholder) and getattr(node, "this", None) is None:
88
+ current_position = qmark_position
89
+ qmark_position += 1
90
+ if current_position in null_positions:
91
+ return _exp.Null()
92
+ return node
93
+
94
+ if isinstance(node, _exp.Placeholder) and getattr(node, "this", None) is not None:
95
+ placeholder_text = str(node.this)
96
+ normalized_text = placeholder_text.lstrip("$")
97
+ if normalized_text.isdigit():
98
+ param_index = int(normalized_text) - 1
99
+ if param_index in null_positions:
100
+ return _exp.Null()
101
+ shift = bisect.bisect_left(sorted_null_positions, param_index)
102
+ new_param_num = param_index - shift + 1
103
+ return _exp.Placeholder(this=f"${new_param_num}")
104
+ return node
105
+
106
+ if isinstance(node, _exp.Parameter) and getattr(node, "this", None) is not None:
107
+ parameter_text = str(node.this)
108
+ if parameter_text.isdigit():
109
+ param_index = int(parameter_text) - 1
110
+ if param_index in null_positions:
111
+ return _exp.Null()
112
+ shift = bisect.bisect_left(sorted_null_positions, param_index)
113
+ new_param_num = param_index - shift + 1
114
+ return _exp.Parameter(this=str(new_param_num))
115
+ return node
116
+
117
+ return node
118
+
119
+ transformed_expression = expression.transform(transform_node)
120
+
121
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, bytearray)):
122
+ cleaned_parameters = [value for index, value in enumerate(parameters) if index not in null_positions]
123
+ elif isinstance(parameters, Mapping):
124
+ cleaned_dict: dict[str, Any] = {}
125
+ next_numeric_index = 1
126
+
127
+ for key, value in parameters.items():
128
+ if value is None:
129
+ continue
130
+ key_kind, normalized_key = normalize_parameter_key(key)
131
+ if key_kind == "index" and isinstance(normalized_key, int):
132
+ cleaned_dict[str(next_numeric_index)] = value
133
+ next_numeric_index += 1
134
+ else:
135
+ cleaned_dict[str(normalized_key)] = value
136
+ cleaned_parameters = cleaned_dict # type: ignore[assignment]
137
+ else:
138
+ cleaned_parameters = parameters
139
+
140
+ return transformed_expression, cleaned_parameters
141
+
142
+
143
+ def _create_literal_expression(value: Any, json_serializer: "Callable[[Any], str]") -> Any:
144
+ """Create a SQLGlot literal expression for the given value."""
145
+ from sqlglot import exp as _exp
146
+
147
+ if value is None:
148
+ return _exp.Null()
149
+ if isinstance(value, bool):
150
+ return _exp.Boolean(this=value)
151
+ if isinstance(value, (int, float)):
152
+ return _exp.Literal.number(str(value))
153
+ if isinstance(value, str):
154
+ return _exp.Literal.string(value)
155
+ if isinstance(value, (list, tuple)):
156
+ items = [_create_literal_expression(item, json_serializer) for item in value]
157
+ return _exp.Array(expressions=items)
158
+ if isinstance(value, dict):
159
+ json_value = json_serializer(value)
160
+ return _exp.Literal.string(json_value)
161
+ return _exp.Literal.string(str(value))
162
+
163
+
164
+ def replace_placeholders_with_literals(
165
+ expression: Any, parameters: Any, *, json_serializer: "Callable[[Any], str]"
166
+ ) -> Any:
167
+ """Replace placeholders in an expression tree with literal values."""
168
+ if not parameters:
169
+ return expression
170
+
171
+ from sqlglot import exp as _exp
172
+
173
+ placeholder_counter = {"index": 0}
174
+
175
+ def resolve_mapping_value(param_name: str, payload: Mapping[str, Any]) -> Any | None:
176
+ candidate_names = (param_name, f"@{param_name}", f":{param_name}", f"${param_name}", f"param_{param_name}")
177
+ for candidate in candidate_names:
178
+ if candidate in payload:
179
+ return getattr(payload[candidate], "value", payload[candidate])
180
+ normalized = param_name.lstrip("@:$")
181
+ if normalized in payload:
182
+ return getattr(payload[normalized], "value", payload[normalized])
183
+ return None
184
+
185
+ def transform(node: Any) -> Any:
186
+ if (
187
+ isinstance(node, _exp.Placeholder)
188
+ and isinstance(parameters, Sequence)
189
+ and not isinstance(parameters, (str, bytes, bytearray))
190
+ ):
191
+ current_index = placeholder_counter["index"]
192
+ placeholder_counter["index"] += 1
193
+ if current_index < len(parameters):
194
+ literal_value = getattr(parameters[current_index], "value", parameters[current_index])
195
+ return _create_literal_expression(literal_value, json_serializer)
196
+ return node
197
+
198
+ if isinstance(node, _exp.Parameter):
199
+ param_name = str(node.this) if getattr(node, "this", None) is not None else ""
200
+
201
+ if isinstance(parameters, Mapping):
202
+ resolved_value = resolve_mapping_value(param_name, parameters)
203
+ if resolved_value is not None:
204
+ return _create_literal_expression(resolved_value, json_serializer)
205
+ return node
206
+
207
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, bytearray)):
208
+ name = param_name
209
+ try:
210
+ if name.startswith("param_"):
211
+ index_value = int(name[6:])
212
+ if 0 <= index_value < len(parameters):
213
+ literal_value = getattr(parameters[index_value], "value", parameters[index_value])
214
+ return _create_literal_expression(literal_value, json_serializer)
215
+ if name.isdigit():
216
+ index_value = int(name)
217
+ if 0 <= index_value < len(parameters):
218
+ literal_value = getattr(parameters[index_value], "value", parameters[index_value])
219
+ return _create_literal_expression(literal_value, json_serializer)
220
+ except (ValueError, AttributeError):
221
+ return node
222
+ return node
223
+
224
+ return node
225
+
226
+ return expression.transform(transform)
@@ -0,0 +1,430 @@
1
+ """Core parameter data structures and utilities."""
2
+
3
+ from collections.abc import Callable, Collection, Generator, Mapping, Sequence
4
+ from datetime import date, datetime, time
5
+ from decimal import Decimal
6
+ from enum import Enum
7
+ from functools import singledispatch
8
+ from types import MappingProxyType
9
+ from typing import Any, Literal
10
+
11
+ from mypy_extensions import mypyc_attr
12
+
13
+ __all__ = (
14
+ "DriverParameterProfile",
15
+ "ParameterInfo",
16
+ "ParameterProcessingResult",
17
+ "ParameterProfile",
18
+ "ParameterStyle",
19
+ "ParameterStyleConfig",
20
+ "TypedParameter",
21
+ "is_iterable_parameters",
22
+ "wrap_with_type",
23
+ )
24
+
25
+
26
+ @mypyc_attr(allow_interpreted_subclasses=False)
27
+ class ParameterStyle(str, Enum):
28
+ """Enumeration of supported SQL parameter placeholder styles."""
29
+
30
+ NONE = "none"
31
+ STATIC = "static"
32
+ QMARK = "qmark"
33
+ NUMERIC = "numeric"
34
+ NAMED_COLON = "named_colon"
35
+ POSITIONAL_COLON = "positional_colon"
36
+ NAMED_AT = "named_at"
37
+ NAMED_DOLLAR = "named_dollar"
38
+ NAMED_PYFORMAT = "pyformat_named"
39
+ POSITIONAL_PYFORMAT = "pyformat_positional"
40
+
41
+
42
+ @mypyc_attr(allow_interpreted_subclasses=False)
43
+ class TypedParameter:
44
+ """Wrapper that preserves original parameter type information."""
45
+
46
+ __slots__ = ("_hash", "original_type", "semantic_name", "value")
47
+
48
+ def __init__(self, value: Any, original_type: "type | None" = None, semantic_name: "str | None" = None) -> None:
49
+ self.value = value
50
+ self.original_type = original_type or type(value)
51
+ self.semantic_name = semantic_name
52
+ self._hash: int | None = None
53
+
54
+ def __hash__(self) -> int:
55
+ if self._hash is None:
56
+ value_id = id(self.value)
57
+ self._hash = hash((value_id, self.original_type, self.semantic_name))
58
+ return self._hash
59
+
60
+ def __eq__(self, other: object) -> bool:
61
+ if not isinstance(other, TypedParameter):
62
+ return False
63
+ return (
64
+ self.value == other.value
65
+ and self.original_type == other.original_type
66
+ and self.semantic_name == other.semantic_name
67
+ )
68
+
69
+ def __repr__(self) -> str:
70
+ name_part = f", semantic_name='{self.semantic_name}'" if self.semantic_name else ""
71
+ return f"TypedParameter({self.value!r}, original_type={self.original_type.__name__}{name_part})"
72
+
73
+
74
+ @singledispatch
75
+ def _wrap_parameter_by_type(value: Any, semantic_name: "str | None" = None) -> Any:
76
+ return value
77
+
78
+
79
+ @_wrap_parameter_by_type.register
80
+ def _(value: bool, semantic_name: "str | None" = None) -> "TypedParameter":
81
+ return TypedParameter(value, bool, semantic_name)
82
+
83
+
84
+ @_wrap_parameter_by_type.register
85
+ def _(value: Decimal, semantic_name: "str | None" = None) -> "TypedParameter":
86
+ return TypedParameter(value, Decimal, semantic_name)
87
+
88
+
89
+ @_wrap_parameter_by_type.register
90
+ def _(value: datetime, semantic_name: "str | None" = None) -> "TypedParameter":
91
+ return TypedParameter(value, datetime, semantic_name)
92
+
93
+
94
+ @_wrap_parameter_by_type.register
95
+ def _(value: date, semantic_name: "str | None" = None) -> "TypedParameter":
96
+ return TypedParameter(value, date, semantic_name)
97
+
98
+
99
+ @_wrap_parameter_by_type.register
100
+ def _(value: time, semantic_name: "str | None" = None) -> "TypedParameter":
101
+ return TypedParameter(value, time, semantic_name)
102
+
103
+
104
+ @_wrap_parameter_by_type.register
105
+ def _(value: bytes, semantic_name: "str | None" = None) -> "TypedParameter":
106
+ return TypedParameter(value, bytes, semantic_name)
107
+
108
+
109
+ @mypyc_attr(allow_interpreted_subclasses=False)
110
+ class ParameterInfo:
111
+ """Metadata describing a single detected SQL parameter."""
112
+
113
+ __slots__ = ("name", "ordinal", "placeholder_text", "position", "style")
114
+
115
+ def __init__(
116
+ self, name: "str | None", style: "ParameterStyle", position: int, ordinal: int, placeholder_text: str
117
+ ) -> None:
118
+ self.name = name
119
+ self.style = style
120
+ self.position = position
121
+ self.ordinal = ordinal
122
+ self.placeholder_text = placeholder_text
123
+
124
+ def __repr__(self) -> str:
125
+ return (
126
+ "ParameterInfo("
127
+ f"name={self.name!r}, style={self.style!r}, position={self.position}, "
128
+ f"ordinal={self.ordinal}, placeholder_text={self.placeholder_text!r}"
129
+ ")"
130
+ )
131
+
132
+
133
+ @mypyc_attr(allow_interpreted_subclasses=False)
134
+ class ParameterStyleConfig:
135
+ """Configuration describing parameter behaviour for a statement."""
136
+
137
+ __slots__ = (
138
+ "allow_mixed_parameter_styles",
139
+ "ast_transformer",
140
+ "default_execution_parameter_style",
141
+ "default_parameter_style",
142
+ "has_native_list_expansion",
143
+ "json_deserializer",
144
+ "json_serializer",
145
+ "needs_static_script_compilation",
146
+ "output_transformer",
147
+ "preserve_original_params_for_many",
148
+ "preserve_parameter_format",
149
+ "supported_execution_parameter_styles",
150
+ "supported_parameter_styles",
151
+ "type_coercion_map",
152
+ )
153
+
154
+ def __init__(
155
+ self,
156
+ default_parameter_style: "ParameterStyle",
157
+ supported_parameter_styles: "Collection[ParameterStyle] | None" = None,
158
+ supported_execution_parameter_styles: "Collection[ParameterStyle] | None" = None,
159
+ default_execution_parameter_style: "ParameterStyle | None" = None,
160
+ type_coercion_map: "Mapping[type, Callable[[Any], Any]] | None" = None,
161
+ has_native_list_expansion: bool = False,
162
+ needs_static_script_compilation: bool = False,
163
+ allow_mixed_parameter_styles: bool = False,
164
+ preserve_parameter_format: bool = True,
165
+ preserve_original_params_for_many: bool = False,
166
+ output_transformer: "Callable[[str, Any], tuple[str, Any]] | None" = None,
167
+ ast_transformer: "Callable[[Any, Any], tuple[Any, Any]] | None" = None,
168
+ json_serializer: "Callable[[Any], str] | None" = None,
169
+ json_deserializer: "Callable[[str], Any] | None" = None,
170
+ ) -> None:
171
+ self.default_parameter_style = default_parameter_style
172
+ self.supported_parameter_styles = frozenset(supported_parameter_styles or (default_parameter_style,))
173
+ self.supported_execution_parameter_styles = (
174
+ frozenset(supported_execution_parameter_styles) if supported_execution_parameter_styles else None
175
+ )
176
+ self.default_execution_parameter_style = default_execution_parameter_style or default_parameter_style
177
+ self.type_coercion_map = dict(type_coercion_map or {})
178
+ self.has_native_list_expansion = has_native_list_expansion
179
+ self.output_transformer = output_transformer
180
+ self.ast_transformer = ast_transformer
181
+ self.needs_static_script_compilation = needs_static_script_compilation
182
+ self.allow_mixed_parameter_styles = allow_mixed_parameter_styles
183
+ self.preserve_parameter_format = preserve_parameter_format
184
+ self.preserve_original_params_for_many = preserve_original_params_for_many
185
+ self.json_serializer = json_serializer
186
+ self.json_deserializer = json_deserializer
187
+
188
+ def __hash__(self) -> int:
189
+ hash_components = (
190
+ self.default_parameter_style.value,
191
+ frozenset(style.value for style in self.supported_parameter_styles),
192
+ (
193
+ frozenset(style.value for style in self.supported_execution_parameter_styles)
194
+ if self.supported_execution_parameter_styles is not None
195
+ else None
196
+ ),
197
+ self.default_execution_parameter_style.value,
198
+ tuple(sorted(self.type_coercion_map.keys(), key=str)) if self.type_coercion_map else None,
199
+ self.has_native_list_expansion,
200
+ self.preserve_original_params_for_many,
201
+ bool(self.output_transformer),
202
+ self.needs_static_script_compilation,
203
+ self.allow_mixed_parameter_styles,
204
+ self.preserve_parameter_format,
205
+ bool(self.ast_transformer),
206
+ self.json_serializer,
207
+ self.json_deserializer,
208
+ )
209
+ return hash(hash_components)
210
+
211
+ def hash(self) -> int:
212
+ """Return the hash value for caching compatibility.
213
+
214
+ Returns:
215
+ Hash value matching :func:`hash` output for this config.
216
+ """
217
+
218
+ return hash(self)
219
+
220
+ def replace(self, **overrides: Any) -> "ParameterStyleConfig":
221
+ data: dict[str, Any] = {
222
+ "default_parameter_style": self.default_parameter_style,
223
+ "supported_parameter_styles": set(self.supported_parameter_styles),
224
+ "supported_execution_parameter_styles": (
225
+ set(self.supported_execution_parameter_styles)
226
+ if self.supported_execution_parameter_styles is not None
227
+ else None
228
+ ),
229
+ "default_execution_parameter_style": self.default_execution_parameter_style,
230
+ "type_coercion_map": dict(self.type_coercion_map),
231
+ "has_native_list_expansion": self.has_native_list_expansion,
232
+ "needs_static_script_compilation": self.needs_static_script_compilation,
233
+ "allow_mixed_parameter_styles": self.allow_mixed_parameter_styles,
234
+ "preserve_parameter_format": self.preserve_parameter_format,
235
+ "preserve_original_params_for_many": self.preserve_original_params_for_many,
236
+ "output_transformer": self.output_transformer,
237
+ "ast_transformer": self.ast_transformer,
238
+ "json_serializer": self.json_serializer,
239
+ "json_deserializer": self.json_deserializer,
240
+ }
241
+ data.update(overrides)
242
+ return ParameterStyleConfig(**data)
243
+
244
+ def with_json_serializers(
245
+ self,
246
+ serializer: "Callable[[Any], str]",
247
+ *,
248
+ tuple_strategy: "Literal['list', 'tuple']" = "list",
249
+ deserializer: "Callable[[str], Any] | None" = None,
250
+ ) -> "ParameterStyleConfig":
251
+ """Return a copy configured with JSON serializers for complex parameters."""
252
+
253
+ if tuple_strategy == "list":
254
+
255
+ def tuple_adapter(value: Any) -> Any:
256
+ return serializer(list(value))
257
+
258
+ elif tuple_strategy == "tuple":
259
+
260
+ def tuple_adapter(value: Any) -> Any:
261
+ return serializer(value)
262
+
263
+ else:
264
+ msg = f"Unsupported tuple_strategy: {tuple_strategy}"
265
+ raise ValueError(msg)
266
+
267
+ updated_type_map = dict(self.type_coercion_map)
268
+ updated_type_map[dict] = serializer
269
+ updated_type_map[list] = serializer
270
+ updated_type_map[tuple] = tuple_adapter
271
+
272
+ return self.replace(
273
+ type_coercion_map=updated_type_map,
274
+ json_serializer=serializer,
275
+ json_deserializer=deserializer or self.json_deserializer,
276
+ )
277
+
278
+
279
+ @mypyc_attr(allow_interpreted_subclasses=False)
280
+ class DriverParameterProfile:
281
+ """Immutable adapter profile describing parameter defaults."""
282
+
283
+ __slots__ = (
284
+ "allow_mixed_parameter_styles",
285
+ "custom_type_coercions",
286
+ "default_ast_transformer",
287
+ "default_dialect",
288
+ "default_execution_style",
289
+ "default_output_transformer",
290
+ "default_style",
291
+ "extras",
292
+ "has_native_list_expansion",
293
+ "json_serializer_strategy",
294
+ "name",
295
+ "needs_static_script_compilation",
296
+ "preserve_original_params_for_many",
297
+ "preserve_parameter_format",
298
+ "statement_kwargs",
299
+ "supported_execution_styles",
300
+ "supported_styles",
301
+ )
302
+
303
+ def __init__(
304
+ self,
305
+ name: str,
306
+ default_style: "ParameterStyle",
307
+ supported_styles: "Collection[ParameterStyle]",
308
+ default_execution_style: "ParameterStyle",
309
+ supported_execution_styles: "Collection[ParameterStyle] | None",
310
+ has_native_list_expansion: bool,
311
+ preserve_parameter_format: bool,
312
+ needs_static_script_compilation: bool,
313
+ allow_mixed_parameter_styles: bool,
314
+ preserve_original_params_for_many: bool,
315
+ json_serializer_strategy: "Literal['driver', 'helper', 'none']",
316
+ custom_type_coercions: "Mapping[type, Callable[[Any], Any]] | None" = None,
317
+ default_output_transformer: "Callable[[str, Any], tuple[str, Any]] | None" = None,
318
+ default_ast_transformer: "Callable[[Any, Any], tuple[Any, Any]] | None" = None,
319
+ extras: "Mapping[str, Any] | None" = None,
320
+ default_dialect: "str | None" = None,
321
+ statement_kwargs: "Mapping[str, Any] | None" = None,
322
+ ) -> None:
323
+ self.name = name
324
+ self.default_style = default_style
325
+ self.supported_styles = frozenset(supported_styles)
326
+ self.default_execution_style = default_execution_style
327
+ self.supported_execution_styles = (
328
+ frozenset(supported_execution_styles) if supported_execution_styles is not None else None
329
+ )
330
+ self.has_native_list_expansion = has_native_list_expansion
331
+ self.preserve_parameter_format = preserve_parameter_format
332
+ self.needs_static_script_compilation = needs_static_script_compilation
333
+ self.allow_mixed_parameter_styles = allow_mixed_parameter_styles
334
+ self.preserve_original_params_for_many = preserve_original_params_for_many
335
+ self.json_serializer_strategy = json_serializer_strategy
336
+ self.custom_type_coercions = (
337
+ MappingProxyType(dict(custom_type_coercions)) if custom_type_coercions else MappingProxyType({})
338
+ )
339
+ self.default_output_transformer = default_output_transformer
340
+ self.default_ast_transformer = default_ast_transformer
341
+ self.extras = MappingProxyType(dict(extras)) if extras else MappingProxyType({})
342
+ self.default_dialect = default_dialect
343
+ self.statement_kwargs = MappingProxyType(dict(statement_kwargs)) if statement_kwargs else MappingProxyType({})
344
+
345
+
346
+ @mypyc_attr(allow_interpreted_subclasses=False)
347
+ class ParameterProfile:
348
+ """Aggregate metadata describing detected parameters."""
349
+
350
+ __slots__ = ("_parameters", "_placeholder_counts", "named_parameters", "reused_ordinals", "styles")
351
+
352
+ def __init__(self, parameters: "Sequence[ParameterInfo] | None" = None) -> None:
353
+ param_tuple: tuple[ParameterInfo, ...] = tuple(parameters) if parameters else ()
354
+ self._parameters = param_tuple
355
+ self.styles = tuple(sorted({param.style.value for param in param_tuple})) if param_tuple else ()
356
+ placeholder_counts: dict[str, int] = {}
357
+ reused_ordinals: list[int] = []
358
+ named_parameters: list[str] = []
359
+
360
+ for param in param_tuple:
361
+ placeholder = param.placeholder_text
362
+ current_count = placeholder_counts.get(placeholder, 0)
363
+ placeholder_counts[placeholder] = current_count + 1
364
+ if current_count:
365
+ reused_ordinals.append(param.ordinal)
366
+ if param.name is not None:
367
+ named_parameters.append(param.name)
368
+
369
+ self._placeholder_counts = placeholder_counts
370
+ self.reused_ordinals = tuple(reused_ordinals)
371
+ self.named_parameters = tuple(named_parameters)
372
+
373
+ @classmethod
374
+ def empty(cls) -> "ParameterProfile":
375
+ return cls(())
376
+
377
+ @property
378
+ def parameters(self) -> "tuple[ParameterInfo, ...]":
379
+ return self._parameters
380
+
381
+ @property
382
+ def total_count(self) -> int:
383
+ return len(self._parameters)
384
+
385
+ def placeholder_count(self, placeholder: str) -> int:
386
+ return self._placeholder_counts.get(placeholder, 0)
387
+
388
+ def is_empty(self) -> bool:
389
+ return not self._parameters
390
+
391
+
392
+ @mypyc_attr(allow_interpreted_subclasses=False)
393
+ class ParameterProcessingResult:
394
+ """Return container for parameter processing output."""
395
+
396
+ __slots__ = ("parameter_profile", "parameters", "sql")
397
+
398
+ def __init__(self, sql: str, parameters: Any, parameter_profile: "ParameterProfile") -> None:
399
+ self.sql = sql
400
+ self.parameters = parameters
401
+ self.parameter_profile = parameter_profile
402
+
403
+ def __iter__(self) -> "Generator[str | Any, Any, None]":
404
+ yield self.sql
405
+ yield self.parameters
406
+
407
+ def __len__(self) -> int:
408
+ return 2
409
+
410
+ def __getitem__(self, index: int) -> Any:
411
+ if index == 0:
412
+ return self.sql
413
+ if index == 1:
414
+ return self.parameters
415
+ msg = "ParameterProcessingResult exposes exactly two positional items"
416
+ raise IndexError(msg)
417
+
418
+
419
+ def is_iterable_parameters(obj: Any) -> bool:
420
+ """Return True when the object behaves like an iterable parameter payload."""
421
+
422
+ return isinstance(obj, (list, tuple, set)) or (
423
+ hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, Mapping))
424
+ )
425
+
426
+
427
+ def wrap_with_type(value: Any, semantic_name: "str | None" = None) -> Any:
428
+ """Wrap value with :class:`TypedParameter` if it benefits downstream processing."""
429
+
430
+ return _wrap_parameter_by_type(value, semantic_name)