sqlspec 0.14.1__py3-none-any.whl → 0.16.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 (159) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +480 -121
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +18 -65
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +830 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +666 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +164 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/cli.py +1 -1
  90. sqlspec/extensions/litestar/config.py +0 -1
  91. sqlspec/extensions/litestar/handlers.py +15 -26
  92. sqlspec/extensions/litestar/plugin.py +18 -16
  93. sqlspec/extensions/litestar/providers.py +17 -52
  94. sqlspec/loader.py +424 -105
  95. sqlspec/migrations/__init__.py +12 -0
  96. sqlspec/migrations/base.py +92 -68
  97. sqlspec/migrations/commands.py +24 -106
  98. sqlspec/migrations/loaders.py +402 -0
  99. sqlspec/migrations/runner.py +49 -51
  100. sqlspec/migrations/tracker.py +31 -44
  101. sqlspec/migrations/utils.py +64 -24
  102. sqlspec/protocols.py +7 -183
  103. sqlspec/storage/__init__.py +1 -1
  104. sqlspec/storage/backends/base.py +37 -40
  105. sqlspec/storage/backends/fsspec.py +136 -112
  106. sqlspec/storage/backends/obstore.py +138 -160
  107. sqlspec/storage/capabilities.py +5 -4
  108. sqlspec/storage/registry.py +57 -106
  109. sqlspec/typing.py +136 -115
  110. sqlspec/utils/__init__.py +2 -3
  111. sqlspec/utils/correlation.py +0 -3
  112. sqlspec/utils/deprecation.py +6 -6
  113. sqlspec/utils/fixtures.py +6 -6
  114. sqlspec/utils/logging.py +0 -2
  115. sqlspec/utils/module_loader.py +7 -12
  116. sqlspec/utils/singleton.py +0 -1
  117. sqlspec/utils/sync_tools.py +17 -38
  118. sqlspec/utils/text.py +12 -51
  119. sqlspec/utils/type_guards.py +443 -232
  120. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
  121. sqlspec-0.16.0.dist-info/RECORD +134 -0
  122. sqlspec/adapters/adbc/transformers.py +0 -108
  123. sqlspec/driver/connection.py +0 -207
  124. sqlspec/driver/mixins/_cache.py +0 -114
  125. sqlspec/driver/mixins/_csv_writer.py +0 -91
  126. sqlspec/driver/mixins/_pipeline.py +0 -508
  127. sqlspec/driver/mixins/_query_tools.py +0 -796
  128. sqlspec/driver/mixins/_result_utils.py +0 -138
  129. sqlspec/driver/mixins/_storage.py +0 -912
  130. sqlspec/driver/mixins/_type_coercion.py +0 -128
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/statement/__init__.py +0 -21
  133. sqlspec/statement/builder/_merge.py +0 -95
  134. sqlspec/statement/cache.py +0 -50
  135. sqlspec/statement/filters.py +0 -625
  136. sqlspec/statement/parameters.py +0 -956
  137. sqlspec/statement/pipelines/__init__.py +0 -210
  138. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  139. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  140. sqlspec/statement/pipelines/context.py +0 -109
  141. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  142. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  143. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  144. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  145. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  146. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  147. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  148. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  149. sqlspec/statement/pipelines/validators/_security.py +0 -967
  150. sqlspec/statement/result.py +0 -435
  151. sqlspec/statement/sql.py +0 -1774
  152. sqlspec/utils/cached_property.py +0 -25
  153. sqlspec/utils/statement_hashing.py +0 -203
  154. sqlspec-0.14.1.dist-info/RECORD +0 -145
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
  159. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,956 +0,0 @@
1
- # ruff: noqa: RUF100, PLR0912, PLR0915, C901, PLR0911, PLR0914
2
- """High-performance SQL parameter conversion system.
3
-
4
- This module provides bulletproof parameter handling for SQL statements,
5
- supporting all major parameter styles with optimized performance.
6
- """
7
-
8
- import logging
9
- import re
10
- from collections.abc import Mapping, Sequence
11
- from dataclasses import dataclass, field
12
- from enum import Enum
13
- from typing import Any, Final, Optional, Union
14
-
15
- from sqlglot import exp
16
- from typing_extensions import TypedDict
17
-
18
- from sqlspec.exceptions import ExtraParameterError, MissingParameterError, ParameterStyleMismatchError
19
- from sqlspec.typing import SQLParameterType
20
-
21
- # Constants
22
- MAX_32BIT_INT: Final[int] = 2147483647
23
-
24
- __all__ = (
25
- "ConvertedParameters",
26
- "ParameterConverter",
27
- "ParameterInfo",
28
- "ParameterStyle",
29
- "ParameterStyleConversionState",
30
- "ParameterValidator",
31
- "SQLParameterType",
32
- "TypedParameter",
33
- )
34
-
35
- logger = logging.getLogger("sqlspec.sql.parameters")
36
-
37
- # Single comprehensive regex that captures all parameter types in one pass
38
- _PARAMETER_REGEX: Final = re.compile(
39
- r"""
40
- # Literals and Comments (these should be matched first and skipped)
41
- (?P<dquote>"(?:[^"\\]|\\.)*") | # Group 1: Double-quoted strings
42
- (?P<squote>'(?:[^'\\]|\\.)*') | # Group 2: Single-quoted strings
43
- # Group 3: Dollar-quoted strings (e.g., $tag$...$tag$ or $$...$$)
44
- # Group 4 (dollar_quote_tag_inner) is the optional tag, back-referenced by \4
45
- (?P<dollar_quoted_string>\$(?P<dollar_quote_tag_inner>\w*)?\$[\s\S]*?\$\4\$) |
46
- (?P<line_comment>--[^\r\n]*) | # Group 5: Line comments
47
- (?P<block_comment>/\*(?:[^*]|\*(?!/))*\*/) | # Group 6: Block comments
48
- # Specific non-parameter tokens that resemble parameters or contain parameter-like chars
49
- # These are matched to prevent them from being identified as parameters.
50
- (?P<pg_q_operator>\?\?|\?\||\?&) | # Group 7: PostgreSQL JSON operators ??, ?|, ?&
51
- (?P<pg_cast>::(?P<cast_type>\w+)) | # Group 8: PostgreSQL ::type casting (cast_type is Group 9)
52
-
53
- # Parameter Placeholders (order can matter if syntax overlaps)
54
- (?P<pyformat_named>%\((?P<pyformat_name>\w+)\)s) | # Group 10: %(name)s (pyformat_name is Group 11)
55
- (?P<pyformat_pos>%s) | # Group 12: %s
56
- # Oracle numeric parameters MUST come before named_colon to match :1, :2, etc.
57
- (?P<positional_colon>:(?P<colon_num>\d+)) | # Group 13: :1, :2 (colon_num is Group 14)
58
- (?P<named_colon>:(?P<colon_name>\w+)) | # Group 15: :name (colon_name is Group 16)
59
- (?P<named_at>@(?P<at_name>\w+)) | # Group 17: @name (at_name is Group 18)
60
- # Group 17: $name or $1 (dollar_param_name is Group 18)
61
- # Differentiation between $name and $1 is handled in Python code using isdigit()
62
- (?P<named_dollar_param>\$(?P<dollar_param_name>\w+)) |
63
- (?P<qmark>\?) # Group 19: ? (now safer due to pg_q_operator rule above)
64
- """,
65
- re.VERBOSE | re.IGNORECASE | re.MULTILINE | re.DOTALL,
66
- )
67
-
68
-
69
- class ParameterStyle(str, Enum):
70
- """Parameter style enumeration with string values."""
71
-
72
- NONE = "none"
73
- STATIC = "static"
74
- QMARK = "qmark"
75
- NUMERIC = "numeric"
76
- NAMED_COLON = "named_colon"
77
- POSITIONAL_COLON = "positional_colon"
78
- NAMED_AT = "named_at"
79
- NAMED_DOLLAR = "named_dollar"
80
- NAMED_PYFORMAT = "pyformat_named"
81
- POSITIONAL_PYFORMAT = "pyformat_positional"
82
-
83
- def __str__(self) -> str:
84
- """String representation for better error messages.
85
-
86
- Returns:
87
- The enum value as a string.
88
- """
89
- return self.value
90
-
91
-
92
- # Define SQLGlot incompatible styles after ParameterStyle enum
93
- SQLGLOT_INCOMPATIBLE_STYLES: Final = {
94
- ParameterStyle.POSITIONAL_PYFORMAT,
95
- ParameterStyle.NAMED_PYFORMAT,
96
- ParameterStyle.POSITIONAL_COLON,
97
- }
98
-
99
-
100
- @dataclass
101
- class ParameterInfo:
102
- """Immutable parameter information with optimal memory usage."""
103
-
104
- name: "Optional[str]"
105
- """Parameter name for named parameters, None for positional."""
106
-
107
- style: "ParameterStyle"
108
- """The parameter style."""
109
-
110
- position: int
111
- """Position in the SQL string (for error reporting)."""
112
-
113
- ordinal: int = field(compare=False)
114
- """Order of appearance in SQL (0-based)."""
115
-
116
- placeholder_text: str = field(compare=False)
117
- """The original text of the parameter."""
118
-
119
-
120
- @dataclass
121
- class TypedParameter:
122
- """Internal container for parameter values with type metadata.
123
-
124
- This class preserves complete type information from SQL literals and user-provided
125
- parameters, enabling proper type coercion for each database adapter.
126
-
127
- Note:
128
- This is an internal class. Users never create TypedParameter objects directly.
129
- The system automatically wraps parameters with type information.
130
- """
131
-
132
- value: Any
133
- """The actual parameter value."""
134
-
135
- sqlglot_type: "exp.DataType"
136
- """Full SQLGlot DataType instance with all type details."""
137
-
138
- type_hint: str
139
- """Simple string hint for adapter type coercion (e.g., 'integer', 'decimal', 'json')."""
140
-
141
- semantic_name: "Optional[str]" = None
142
- """Optional semantic name derived from SQL context (e.g., 'user_id', 'email')."""
143
-
144
- def __hash__(self) -> int:
145
- """Make TypedParameter hashable for use in cache keys.
146
-
147
- We hash based on the value and type_hint, which are the key attributes
148
- that affect SQL compilation and parameter handling.
149
- """
150
- if isinstance(self.value, (list, dict)):
151
- value_hash = hash(repr(self.value))
152
- else:
153
- try:
154
- value_hash = hash(self.value)
155
- except TypeError:
156
- value_hash = hash(repr(self.value))
157
-
158
- return hash((value_hash, self.type_hint, self.semantic_name))
159
-
160
-
161
- class ParameterStyleInfo(TypedDict, total=False):
162
- """Information about SQL parameter style transformation."""
163
-
164
- was_converted: bool
165
- placeholder_map: dict[str, Union[str, int]]
166
- original_styles: list[ParameterStyle]
167
-
168
-
169
- @dataclass
170
- class ParameterStyleConversionState:
171
- """Encapsulates all information about parameter style transformation.
172
-
173
- This class provides a single source of truth for parameter style conversions,
174
- making it easier to track and reverse transformations applied for SQLGlot compatibility.
175
- """
176
-
177
- was_transformed: bool = False
178
- """Whether parameter transformation was applied."""
179
-
180
- original_styles: list[ParameterStyle] = field(default_factory=list)
181
- """Original parameter style(s) detected in the SQL."""
182
-
183
- transformation_style: Optional[ParameterStyle] = None
184
- """Target style used for transformation (if transformed)."""
185
-
186
- placeholder_map: dict[str, Union[str, int]] = field(default_factory=dict)
187
- """Mapping from transformed names to original names/positions."""
188
-
189
- reverse_map: dict[Union[str, int], str] = field(default_factory=dict)
190
- """Reverse mapping for quick lookups."""
191
-
192
- original_param_info: list["ParameterInfo"] = field(default_factory=list)
193
- """Original parameter info before conversion."""
194
-
195
- def __post_init__(self) -> None:
196
- """Build reverse map if not provided."""
197
- if self.placeholder_map and not self.reverse_map:
198
- self.reverse_map = {v: k for k, v in self.placeholder_map.items()}
199
-
200
-
201
- @dataclass
202
- class ConvertedParameters:
203
- """Result of parameter conversion with clear structure."""
204
-
205
- transformed_sql: str
206
- """SQL after any necessary transformations."""
207
-
208
- parameter_info: list["ParameterInfo"]
209
- """Information about parameters found in the SQL."""
210
-
211
- merged_parameters: "SQLParameterType"
212
- """Parameters after merging from various sources."""
213
-
214
- conversion_state: ParameterStyleConversionState
215
- """Complete conversion state for tracking conversions."""
216
-
217
-
218
- @dataclass
219
- class ParameterValidator:
220
- """Parameter validation."""
221
-
222
- def __post_init__(self) -> None:
223
- """Initialize validator."""
224
- self._parameter_cache: dict[str, list[ParameterInfo]] = {}
225
-
226
- @staticmethod
227
- def _create_parameter_info_from_match(match: "re.Match[str]", ordinal: int) -> "Optional[ParameterInfo]":
228
- if (
229
- match.group("dquote")
230
- or match.group("squote")
231
- or match.group("dollar_quoted_string")
232
- or match.group("line_comment")
233
- or match.group("block_comment")
234
- or match.group("pg_q_operator")
235
- or match.group("pg_cast")
236
- ):
237
- return None
238
-
239
- position = match.start()
240
- name: Optional[str] = None
241
- style: ParameterStyle
242
-
243
- if match.group("pyformat_named"):
244
- name = match.group("pyformat_name")
245
- style = ParameterStyle.NAMED_PYFORMAT
246
- elif match.group("pyformat_pos"):
247
- style = ParameterStyle.POSITIONAL_PYFORMAT
248
- elif match.group("positional_colon"):
249
- name = match.group("colon_num")
250
- style = ParameterStyle.POSITIONAL_COLON
251
- elif match.group("named_colon"):
252
- name = match.group("colon_name")
253
- style = ParameterStyle.NAMED_COLON
254
- elif match.group("named_at"):
255
- name = match.group("at_name")
256
- style = ParameterStyle.NAMED_AT
257
- elif match.group("named_dollar_param"):
258
- name_candidate = match.group("dollar_param_name")
259
- if not name_candidate.isdigit():
260
- name = name_candidate
261
- style = ParameterStyle.NAMED_DOLLAR
262
- else:
263
- name = name_candidate # Keep the numeric value as name for NUMERIC style
264
- style = ParameterStyle.NUMERIC
265
- elif match.group("qmark"):
266
- style = ParameterStyle.QMARK
267
- else:
268
- logger.warning(
269
- "Unhandled SQL token pattern found by regex. Matched group: %s. Token: '%s'",
270
- match.lastgroup,
271
- match.group(0),
272
- )
273
- return None
274
-
275
- return ParameterInfo(name, style, position, ordinal, match.group(0))
276
-
277
- def extract_parameters(self, sql: str) -> "list[ParameterInfo]":
278
- """Extract all parameters from SQL with single-pass parsing.
279
-
280
- Args:
281
- sql: SQL string to analyze
282
-
283
- Returns:
284
- List of ParameterInfo objects in order of appearance
285
- """
286
- if sql in self._parameter_cache:
287
- return self._parameter_cache[sql]
288
-
289
- parameters: list[ParameterInfo] = []
290
- ordinal = 0
291
- for match in _PARAMETER_REGEX.finditer(sql):
292
- param_info = self._create_parameter_info_from_match(match, ordinal)
293
- if param_info:
294
- parameters.append(param_info)
295
- ordinal += 1
296
-
297
- self._parameter_cache[sql] = parameters
298
- return parameters
299
-
300
- @staticmethod
301
- def get_parameter_style(parameters_info: "list[ParameterInfo]") -> "ParameterStyle":
302
- """Determine overall parameter style from parameter list.
303
-
304
- This typically identifies the dominant style for user-facing messages or general classification.
305
- It differs from `determine_parameter_input_type` which is about expected Python type for params.
306
-
307
- Args:
308
- parameters_info: List of extracted parameters
309
-
310
- Returns:
311
- Overall parameter style
312
- """
313
- if not parameters_info:
314
- return ParameterStyle.NONE
315
- is_pyformat_named = any(p.style == ParameterStyle.NAMED_PYFORMAT for p in parameters_info)
316
- is_pyformat_positional = any(p.style == ParameterStyle.POSITIONAL_PYFORMAT for p in parameters_info)
317
-
318
- if is_pyformat_named:
319
- return ParameterStyle.NAMED_PYFORMAT
320
- if is_pyformat_positional:
321
- return ParameterStyle.POSITIONAL_PYFORMAT
322
- has_named = any(
323
- p.style
324
- in {
325
- ParameterStyle.NAMED_COLON,
326
- ParameterStyle.POSITIONAL_COLON,
327
- ParameterStyle.NAMED_AT,
328
- ParameterStyle.NAMED_DOLLAR,
329
- }
330
- for p in parameters_info
331
- )
332
- has_positional = any(p.style in {ParameterStyle.QMARK, ParameterStyle.NUMERIC} for p in parameters_info)
333
- if has_named:
334
- for p_style in (
335
- ParameterStyle.NAMED_COLON,
336
- ParameterStyle.POSITIONAL_COLON,
337
- ParameterStyle.NAMED_DOLLAR,
338
- ParameterStyle.NAMED_AT,
339
- ):
340
- if any(p.style == p_style for p in parameters_info):
341
- return p_style
342
- return ParameterStyle.NAMED_COLON
343
-
344
- if has_positional:
345
- if any(p.style == ParameterStyle.NUMERIC for p in parameters_info):
346
- return ParameterStyle.NUMERIC
347
- return ParameterStyle.QMARK
348
-
349
- return ParameterStyle.NONE
350
-
351
- @staticmethod
352
- def determine_parameter_input_type(parameters_info: "list[ParameterInfo]") -> "Optional[type]":
353
- """Determine if user-provided parameters should be a dict, list/tuple, or None.
354
-
355
- - If any parameter placeholder implies a name (e.g., :name, %(name)s), a dict is expected.
356
- - If all parameter placeholders are strictly positional (e.g., ?, %s, $1), a list/tuple is expected.
357
- - If no parameters, None is expected.
358
-
359
- Args:
360
- parameters_info: List of extracted ParameterInfo objects.
361
-
362
- Returns:
363
- `dict` if named parameters are expected, `list` if positional, `None` if no parameters.
364
- """
365
- if not parameters_info:
366
- return None
367
-
368
- if all(p.style == ParameterStyle.POSITIONAL_COLON for p in parameters_info):
369
- return list
370
-
371
- if any(
372
- p.name is not None and p.style not in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
373
- for p in parameters_info
374
- ):
375
- return dict
376
- if all(
377
- p.name is None or p.style in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
378
- for p in parameters_info
379
- ):
380
- return list
381
- # This case implies a mix of parameters where some have names and some don't,
382
- # but not fitting the clear dict/list categories above.
383
- # Example: SQL like "SELECT :name, ?" - this is problematic and usually not supported directly.
384
- # Standard DBAPIs typically don't mix named and unnamed placeholders in the same query (outside pyformat).
385
- logger.warning(
386
- "Ambiguous parameter structure for determining input type. "
387
- "Query might contain a mix of named and unnamed styles not typically supported together."
388
- )
389
- return dict
390
-
391
- def validate_parameters(
392
- self,
393
- parameters_info: "list[ParameterInfo]",
394
- provided_params: "SQLParameterType",
395
- original_sql_for_error: "Optional[str]" = None,
396
- ) -> None:
397
- """Validate provided parameters against SQL requirements.
398
-
399
- Args:
400
- parameters_info: Extracted parameter info
401
- provided_params: Parameters provided by user
402
- original_sql_for_error: Original SQL for error context
403
-
404
- Raises:
405
- ParameterStyleMismatchError: When style doesn't match
406
- """
407
- expected_input_type = self.determine_parameter_input_type(parameters_info)
408
- if provided_params is None and parameters_info:
409
- return
410
-
411
- if (
412
- len(parameters_info) == 1
413
- and provided_params is not None
414
- and not isinstance(provided_params, (dict, list, tuple, Mapping))
415
- and (not isinstance(provided_params, Sequence) or isinstance(provided_params, (str, bytes)))
416
- ):
417
- return
418
-
419
- if expected_input_type is dict:
420
- if not isinstance(provided_params, Mapping):
421
- msg = (
422
- f"SQL expects named parameters (dictionary/mapping), but received {type(provided_params).__name__}"
423
- )
424
- raise ParameterStyleMismatchError(msg, original_sql_for_error)
425
- self._validate_named_parameters(parameters_info, provided_params, original_sql_for_error)
426
- elif expected_input_type is list:
427
- if not isinstance(provided_params, Sequence) or isinstance(provided_params, (str, bytes)):
428
- msg = f"SQL expects positional parameters (list/tuple), but received {type(provided_params).__name__}"
429
- raise ParameterStyleMismatchError(msg, original_sql_for_error)
430
- self._validate_positional_parameters(parameters_info, provided_params, original_sql_for_error)
431
- elif expected_input_type is None and parameters_info:
432
- logger.error(
433
- "Parameter validation encountered an unexpected state: placeholders exist, "
434
- "but expected input type could not be determined. SQL: %s",
435
- original_sql_for_error,
436
- )
437
- msg = "Could not determine expected parameter type for the given SQL."
438
- raise ParameterStyleMismatchError(msg, original_sql_for_error)
439
-
440
- @staticmethod
441
- def _has_actual_params(params: SQLParameterType) -> bool:
442
- """Check if parameters contain actual values.
443
-
444
- Returns:
445
- True if parameters contain actual values.
446
- """
447
- if isinstance(params, (Mapping, Sequence)) and not isinstance(params, (str, bytes)):
448
- return bool(params) # True for non-empty dict/list/tuple
449
- return params is not None # True for scalar values other than None
450
-
451
- @staticmethod
452
- def _validate_named_parameters(
453
- parameters_info: "list[ParameterInfo]", provided_params: "Mapping[str, Any]", original_sql: "Optional[str]"
454
- ) -> None:
455
- """Validate named parameters.
456
-
457
- Raises:
458
- MissingParameterError: When required parameters are missing
459
- ExtraParameterError: When extra parameters are provided
460
- """
461
- required_names = {p.name for p in parameters_info if p.name is not None}
462
- provided_names = set(provided_params.keys())
463
-
464
- positional_count = sum(1 for p in parameters_info if p.name is None)
465
- expected_positional_names = {f"arg_{p.ordinal}" for p in parameters_info if p.name is None}
466
- if positional_count > 0 and required_names:
467
- all_expected_names = required_names | expected_positional_names
468
-
469
- missing = all_expected_names - provided_names
470
- if missing:
471
- msg = f"Missing required parameters: {sorted(missing)}"
472
- raise MissingParameterError(msg, original_sql)
473
-
474
- extra = provided_names - all_expected_names
475
- if extra:
476
- msg = f"Extra parameters provided: {sorted(extra)}"
477
- raise ExtraParameterError(msg, original_sql)
478
- else:
479
- missing = required_names - provided_names
480
- if missing:
481
- msg = f"Missing required named parameters: {sorted(missing)}"
482
- raise MissingParameterError(msg, original_sql)
483
-
484
- extra = provided_names - required_names
485
- if extra:
486
- msg = f"Extra parameters provided: {sorted(extra)}"
487
- raise ExtraParameterError(msg, original_sql)
488
-
489
- @staticmethod
490
- def _validate_positional_parameters(
491
- parameters_info: "list[ParameterInfo]", provided_params: "Sequence[Any]", original_sql: "Optional[str]"
492
- ) -> None:
493
- """Validate positional parameters.
494
-
495
- Raises:
496
- MissingParameterError: When required parameters are missing.
497
- ExtraParameterError: When extra parameters are provided.
498
- """
499
- expected_positional_params_count = sum(
500
- 1
501
- for p in parameters_info
502
- if p.name is None or p.style in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
503
- )
504
- actual_count = len(provided_params)
505
-
506
- if actual_count != expected_positional_params_count:
507
- if actual_count > expected_positional_params_count:
508
- msg = (
509
- f"SQL requires {expected_positional_params_count} positional parameters "
510
- f"but {actual_count} were provided."
511
- )
512
- raise ExtraParameterError(msg, original_sql)
513
-
514
- msg = (
515
- f"SQL requires {expected_positional_params_count} positional parameters "
516
- f"but {actual_count} were provided."
517
- )
518
- raise MissingParameterError(msg, original_sql)
519
-
520
-
521
- @dataclass
522
- class ParameterConverter:
523
- """Parameter parameter conversion with caching and validation."""
524
-
525
- def __init__(self) -> None:
526
- """Initialize converter with validator."""
527
- self.validator = ParameterValidator()
528
-
529
- @staticmethod
530
- def _transform_sql_for_parsing(
531
- original_sql: str, parameters_info: "list[ParameterInfo]"
532
- ) -> tuple[str, dict[str, Union[str, int]]]:
533
- """Transform SQL to use unique named placeholders for sqlglot parsing.
534
-
535
- Args:
536
- original_sql: The original SQL string.
537
- parameters_info: List of ParameterInfo objects for the SQL.
538
- Assumed to be sorted by position as extracted.
539
-
540
- Returns:
541
- A tuple containing:
542
- - transformed_sql: SQL string with unique named placeholders (e.g., :param_0).
543
- - placeholder_map: Dictionary mapping new unique names to original names or ordinal index.
544
- """
545
- transformed_sql_parts = []
546
- placeholder_map: dict[str, Union[str, int]] = {}
547
- current_pos = 0
548
- for i, p_info in enumerate(parameters_info):
549
- transformed_sql_parts.append(original_sql[current_pos : p_info.position])
550
-
551
- unique_placeholder_name = f":param_{i}"
552
- map_key = f"param_{i}"
553
-
554
- if p_info.name:
555
- placeholder_map[map_key] = p_info.name
556
- else:
557
- placeholder_map[map_key] = p_info.ordinal
558
-
559
- transformed_sql_parts.append(unique_placeholder_name)
560
- current_pos = p_info.position + len(p_info.placeholder_text)
561
-
562
- transformed_sql_parts.append(original_sql[current_pos:])
563
- return "".join(transformed_sql_parts), placeholder_map
564
-
565
- def convert_placeholders(
566
- self, sql: str, target_style: "ParameterStyle", parameter_info: "Optional[list[ParameterInfo]]" = None
567
- ) -> str:
568
- """Convert SQL placeholders to a target style.
569
-
570
- Args:
571
- sql: The SQL string with placeholders
572
- target_style: The target parameter style to convert to
573
- parameter_info: Optional list of parameter info (will be extracted if not provided)
574
-
575
- Returns:
576
- SQL string with converted placeholders
577
- """
578
- if parameter_info is None:
579
- parameter_info = self.validator.extract_parameters(sql)
580
-
581
- if not parameter_info:
582
- return sql
583
-
584
- result_parts = []
585
- current_pos = 0
586
-
587
- for i, param in enumerate(parameter_info):
588
- result_parts.append(sql[current_pos : param.position])
589
-
590
- if target_style == ParameterStyle.QMARK:
591
- placeholder = "?"
592
- elif target_style == ParameterStyle.NUMERIC:
593
- placeholder = f"${i + 1}"
594
- elif target_style == ParameterStyle.POSITIONAL_PYFORMAT:
595
- placeholder = "%s"
596
- elif target_style == ParameterStyle.NAMED_COLON:
597
- if param.style in {
598
- ParameterStyle.POSITIONAL_COLON,
599
- ParameterStyle.QMARK,
600
- ParameterStyle.NUMERIC,
601
- ParameterStyle.POSITIONAL_PYFORMAT,
602
- }:
603
- name = f"param_{i}"
604
- else:
605
- name = param.name or f"param_{i}"
606
- placeholder = f":{name}"
607
- elif target_style == ParameterStyle.NAMED_PYFORMAT:
608
- if param.style in {
609
- ParameterStyle.POSITIONAL_COLON,
610
- ParameterStyle.QMARK,
611
- ParameterStyle.NUMERIC,
612
- ParameterStyle.POSITIONAL_PYFORMAT,
613
- }:
614
- name = f"param_{i}"
615
- else:
616
- name = param.name or f"param_{i}"
617
- placeholder = f"%({name})s"
618
- elif target_style == ParameterStyle.NAMED_AT:
619
- if param.style in {
620
- ParameterStyle.POSITIONAL_COLON,
621
- ParameterStyle.QMARK,
622
- ParameterStyle.NUMERIC,
623
- ParameterStyle.POSITIONAL_PYFORMAT,
624
- }:
625
- name = f"param_{i}"
626
- else:
627
- name = param.name or f"param_{i}"
628
- placeholder = f"@{name}"
629
- elif target_style == ParameterStyle.NAMED_DOLLAR:
630
- if param.style in {
631
- ParameterStyle.POSITIONAL_COLON,
632
- ParameterStyle.QMARK,
633
- ParameterStyle.NUMERIC,
634
- ParameterStyle.POSITIONAL_PYFORMAT,
635
- }:
636
- name = f"param_{i}"
637
- else:
638
- name = param.name or f"param_{i}"
639
- placeholder = f"${name}"
640
- elif target_style == ParameterStyle.POSITIONAL_COLON:
641
- placeholder = f":{i + 1}"
642
- else:
643
- placeholder = param.placeholder_text
644
-
645
- result_parts.append(placeholder)
646
- current_pos = param.position + len(param.placeholder_text)
647
-
648
- result_parts.append(sql[current_pos:])
649
-
650
- return "".join(result_parts)
651
-
652
- def convert_parameters(
653
- self,
654
- sql: str,
655
- parameters: "SQLParameterType" = None,
656
- args: "Optional[Sequence[Any]]" = None,
657
- kwargs: "Optional[Mapping[str, Any]]" = None,
658
- validate: bool = True,
659
- ) -> ConvertedParameters:
660
- """Convert and merge parameters, and transform SQL for parsing.
661
-
662
- Args:
663
- sql: SQL string to analyze
664
- parameters: Primary parameters
665
- args: Positional arguments (for compatibility)
666
- kwargs: Keyword arguments
667
- validate: Whether to validate parameters
668
-
669
- Returns:
670
- ConvertedParameters object with all conversion information
671
- """
672
- parameters_info = self.validator.extract_parameters(sql)
673
-
674
- needs_conversion = any(p.style in SQLGLOT_INCOMPATIBLE_STYLES for p in parameters_info)
675
-
676
- has_positional = any(p.name is None for p in parameters_info)
677
- has_named = any(p.name is not None for p in parameters_info)
678
- has_mixed_styles = has_positional and has_named
679
-
680
- if has_mixed_styles and args and kwargs and parameters is None:
681
- merged_params = self._merge_mixed_parameters(parameters_info, args, kwargs)
682
- else:
683
- merged_params = self.merge_parameters(parameters, args, kwargs) # type: ignore[assignment]
684
-
685
- if validate:
686
- self.validator.validate_parameters(parameters_info, merged_params, sql)
687
- if needs_conversion:
688
- transformed_sql, placeholder_map = self._transform_sql_for_parsing(sql, parameters_info)
689
- conversion_state = ParameterStyleConversionState(
690
- was_transformed=True,
691
- original_styles=list({p.style for p in parameters_info}),
692
- transformation_style=ParameterStyle.NAMED_COLON,
693
- placeholder_map=placeholder_map,
694
- original_param_info=parameters_info,
695
- )
696
- else:
697
- transformed_sql = sql
698
- conversion_state = ParameterStyleConversionState(
699
- was_transformed=False,
700
- original_styles=list({p.style for p in parameters_info}),
701
- original_param_info=parameters_info,
702
- )
703
-
704
- return ConvertedParameters(
705
- transformed_sql=transformed_sql,
706
- parameter_info=parameters_info,
707
- merged_parameters=merged_params,
708
- conversion_state=conversion_state,
709
- )
710
-
711
- @staticmethod
712
- def _merge_mixed_parameters(
713
- parameters_info: "list[ParameterInfo]", args: "Sequence[Any]", kwargs: "Mapping[str, Any]"
714
- ) -> dict[str, Any]:
715
- """Merge args and kwargs for mixed parameter styles.
716
-
717
- Args:
718
- parameters_info: List of parameter information from SQL
719
- args: Positional arguments
720
- kwargs: Keyword arguments
721
-
722
- Returns:
723
- Dictionary with merged parameters
724
- """
725
- merged: dict[str, Any] = {}
726
-
727
- merged.update(kwargs)
728
-
729
- positional_count = 0
730
- for param_info in parameters_info:
731
- if param_info.name is None and positional_count < len(args):
732
- param_name = f"arg_{param_info.ordinal}"
733
- merged[param_name] = args[positional_count]
734
- positional_count += 1
735
-
736
- return merged
737
-
738
- @staticmethod
739
- def merge_parameters(
740
- parameters: "SQLParameterType", args: "Optional[Sequence[Any]]", kwargs: "Optional[Mapping[str, Any]]"
741
- ) -> "SQLParameterType":
742
- """Merge parameters from different sources with proper precedence.
743
-
744
- Precedence order (highest to lowest):
745
- 1. parameters (primary source - always wins)
746
- 2. kwargs (secondary source)
747
- 3. args (only used if parameters is None and no kwargs)
748
-
749
- Returns:
750
- Merged parameters as a dictionary or list/tuple, or None.
751
- """
752
- # If parameters is provided, it takes precedence over everything
753
- if parameters is not None:
754
- return parameters
755
-
756
- if kwargs is not None:
757
- return dict(kwargs)
758
-
759
- if args is not None:
760
- return list(args)
761
-
762
- return None
763
-
764
- @staticmethod
765
- def wrap_parameters_with_types(
766
- parameters: "SQLParameterType",
767
- parameters_info: "list[ParameterInfo]", # noqa: ARG004
768
- ) -> "SQLParameterType":
769
- """Wrap user-provided parameters with TypedParameter objects when needed.
770
-
771
- This is called internally by the SQL processing pipeline after parameter
772
- extraction and merging. It preserves the original parameter structure
773
- while adding type information where beneficial.
774
-
775
- Args:
776
- parameters: User-provided parameters (dict, list, or scalar)
777
- parameters_info: Extracted parameter information from SQL
778
-
779
- Returns:
780
- Parameters with TypedParameter wrapping where appropriate
781
- """
782
- if parameters is None:
783
- return None
784
-
785
- # Import here to avoid circular imports
786
- from datetime import date, datetime, time
787
- from decimal import Decimal
788
-
789
- def infer_type_from_value(value: Any) -> tuple[str, "exp.DataType"]:
790
- """Infer SQL type hint and SQLGlot DataType from Python value."""
791
-
792
- # None/NULL
793
- if value is None:
794
- return "null", exp.DataType.build("NULL")
795
- if isinstance(value, bool):
796
- return "boolean", exp.DataType.build("BOOLEAN")
797
- if isinstance(value, int) and not isinstance(value, bool):
798
- if abs(value) > MAX_32BIT_INT:
799
- return "bigint", exp.DataType.build("BIGINT")
800
- return "integer", exp.DataType.build("INT")
801
- if isinstance(value, float):
802
- return "float", exp.DataType.build("FLOAT")
803
- if isinstance(value, Decimal):
804
- return "decimal", exp.DataType.build("DECIMAL")
805
- if isinstance(value, datetime):
806
- return "timestamp", exp.DataType.build("TIMESTAMP")
807
- if isinstance(value, date):
808
- return "date", exp.DataType.build("DATE")
809
- if isinstance(value, time):
810
- return "time", exp.DataType.build("TIME")
811
- if isinstance(value, dict):
812
- return "json", exp.DataType.build("JSON")
813
- if isinstance(value, (list, tuple)):
814
- return "array", exp.DataType.build("ARRAY")
815
- if isinstance(value, str):
816
- return "string", exp.DataType.build("VARCHAR")
817
- if isinstance(value, bytes):
818
- return "binary", exp.DataType.build("BINARY")
819
- return "string", exp.DataType.build("VARCHAR")
820
-
821
- def wrap_value(value: Any, semantic_name: Optional[str] = None) -> Any:
822
- """Wrap a single value with TypedParameter if beneficial."""
823
- # Don't wrap if already a TypedParameter
824
- if hasattr(value, "__class__") and value.__class__.__name__ == "TypedParameter":
825
- return value
826
-
827
- # Don't wrap simple scalar types unless they need special handling
828
- if isinstance(value, (str, int, float)) and not isinstance(value, bool):
829
- # For simple types, only wrap if we have special type needs
830
- # (e.g., bigint, decimal precision, etc.)
831
- if isinstance(value, int) and abs(value) > MAX_32BIT_INT:
832
- # Wrap large integers as bigint
833
- type_hint, sqlglot_type = infer_type_from_value(value)
834
- return TypedParameter(
835
- value=value, sqlglot_type=sqlglot_type, type_hint=type_hint, semantic_name=semantic_name
836
- )
837
- # Otherwise, return unwrapped for performance
838
- return value
839
-
840
- # Wrap complex types and types needing special handling
841
- if isinstance(value, (datetime, date, time, Decimal, dict, list, tuple, bytes, bool, type(None))):
842
- type_hint, sqlglot_type = infer_type_from_value(value)
843
- return TypedParameter(
844
- value=value, sqlglot_type=sqlglot_type, type_hint=type_hint, semantic_name=semantic_name
845
- )
846
-
847
- # Default: return unwrapped
848
- return value
849
-
850
- # Handle different parameter structures
851
- if isinstance(parameters, dict):
852
- # Wrap dict values selectively
853
- wrapped_dict = {}
854
- for key, value in parameters.items():
855
- wrapped_dict[key] = wrap_value(value, semantic_name=key)
856
- return wrapped_dict
857
-
858
- if isinstance(parameters, (list, tuple)):
859
- # Wrap list/tuple values selectively
860
- wrapped_list: list[Any] = []
861
- for i, value in enumerate(parameters):
862
- # Try to get semantic name from parameters_info if available
863
- semantic_name = None
864
- if parameters_info and i < len(parameters_info) and parameters_info[i].name:
865
- semantic_name = parameters_info[i].name
866
- wrapped_list.append(wrap_value(value, semantic_name=semantic_name))
867
- return wrapped_list if isinstance(parameters, list) else tuple(wrapped_list)
868
-
869
- # Single scalar parameter
870
- semantic_name = None
871
- if parameters_info and parameters_info[0].name:
872
- semantic_name = parameters_info[0].name
873
- return wrap_value(parameters, semantic_name=semantic_name)
874
-
875
- def _convert_sql_placeholders(
876
- self, rendered_sql: str, final_parameter_info: "list[ParameterInfo]", target_style: "ParameterStyle"
877
- ) -> str:
878
- """Internal method to convert SQL from canonical format to target style.
879
-
880
- Args:
881
- rendered_sql: SQL with canonical placeholders (:param_N)
882
- final_parameter_info: Complete parameter info list
883
- target_style: Target parameter style
884
-
885
- Returns:
886
- SQL with target style placeholders
887
- """
888
- canonical_params = self.validator.extract_parameters(rendered_sql)
889
-
890
- # When we have more canonical parameters than final_parameter_info,
891
- # it's likely because the ParameterizeLiterals transformer added extra parameters.
892
- # We need to denormalize ALL parameters to ensure proper placeholder conversion.
893
- # The final_parameter_info only contains the original parameters, but we need
894
- # to handle all placeholders in the SQL (including those added by transformers).
895
- if len(canonical_params) > len(final_parameter_info):
896
- # Extend final_parameter_info to match canonical_params
897
- # Use the canonical param info for the extra parameters
898
- final_parameter_info = list(final_parameter_info)
899
- for i in range(len(final_parameter_info), len(canonical_params)):
900
- # Create a synthetic ParameterInfo for the extra parameter
901
- canonical = canonical_params[i]
902
- # Use the ordinal from the canonical parameter
903
- final_parameter_info.append(canonical)
904
- elif len(canonical_params) < len(final_parameter_info):
905
- from sqlspec.exceptions import SQLTransformationError
906
-
907
- msg = (
908
- f"Parameter count mismatch during deconversion. "
909
- f"Expected at least {len(final_parameter_info)} parameters, "
910
- f"found {len(canonical_params)} in SQL"
911
- )
912
- raise SQLTransformationError(msg)
913
-
914
- result_sql = rendered_sql
915
-
916
- for i in range(len(canonical_params) - 1, -1, -1):
917
- canonical = canonical_params[i]
918
- source_info = final_parameter_info[i]
919
-
920
- start = canonical.position
921
- end = start + len(canonical.placeholder_text)
922
- new_placeholder = self._get_placeholder_for_style(target_style, source_info)
923
- result_sql = result_sql[:start] + new_placeholder + result_sql[end:]
924
-
925
- return result_sql
926
-
927
- @staticmethod
928
- def _get_placeholder_for_style(target_style: "ParameterStyle", param_info: "ParameterInfo") -> str:
929
- """Generate placeholder text for a specific parameter style.
930
-
931
- Args:
932
- target_style: Target parameter style
933
- param_info: Parameter information
934
-
935
- Returns:
936
- Placeholder string for the target style
937
- """
938
- if target_style == ParameterStyle.QMARK:
939
- return "?"
940
- if target_style == ParameterStyle.NUMERIC:
941
- return f"${param_info.ordinal + 1}"
942
- if target_style == ParameterStyle.NAMED_COLON:
943
- return f":{param_info.name}" if param_info.name else f":arg_{param_info.ordinal}"
944
- if target_style == ParameterStyle.POSITIONAL_COLON:
945
- if param_info.style == ParameterStyle.POSITIONAL_COLON and param_info.name and param_info.name.isdigit():
946
- return f":{param_info.name}"
947
- return f":{param_info.ordinal + 1}"
948
- if target_style == ParameterStyle.NAMED_AT:
949
- return f"@{param_info.name}" if param_info.name else f"@arg_{param_info.ordinal}"
950
- if target_style == ParameterStyle.NAMED_DOLLAR:
951
- return f"${param_info.name}" if param_info.name else f"$arg_{param_info.ordinal}"
952
- if target_style == ParameterStyle.NAMED_PYFORMAT:
953
- return f"%({param_info.name})s" if param_info.name else f"%(arg_{param_info.ordinal})s"
954
- if target_style == ParameterStyle.POSITIONAL_PYFORMAT:
955
- return "%s"
956
- return param_info.placeholder_text