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.
- sqlspec/__init__.py +104 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +312 -0
- sqlspec/_typing.py +784 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +436 -0
- sqlspec/adapters/adbc/data_dictionary.py +537 -0
- sqlspec/adapters/adbc/driver.py +841 -0
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/__init__.py +29 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +310 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +260 -0
- sqlspec/adapters/aiosqlite/driver.py +463 -0
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +500 -0
- sqlspec/adapters/asyncmy/__init__.py +25 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +246 -0
- sqlspec/adapters/asyncmy/data_dictionary.py +241 -0
- sqlspec/adapters/asyncmy/driver.py +632 -0
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +23 -0
- sqlspec/adapters/asyncpg/_type_handlers.py +76 -0
- sqlspec/adapters/asyncpg/_types.py +23 -0
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +460 -0
- sqlspec/adapters/asyncpg/config.py +464 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +321 -0
- sqlspec/adapters/asyncpg/driver.py +720 -0
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +585 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/data_dictionary.py +256 -0
- sqlspec/adapters/bigquery/driver.py +1073 -0
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/__init__.py +24 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +396 -0
- sqlspec/adapters/duckdb/data_dictionary.py +264 -0
- sqlspec/adapters/duckdb/driver.py +604 -0
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +273 -0
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +39 -0
- sqlspec/adapters/oracledb/_uuid_handlers.py +130 -0
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1632 -0
- sqlspec/adapters/oracledb/config.py +469 -0
- sqlspec/adapters/oracledb/data_dictionary.py +717 -0
- sqlspec/adapters/oracledb/driver.py +1493 -0
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +532 -0
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +12 -0
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +483 -0
- sqlspec/adapters/psqlpy/config.py +271 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +179 -0
- sqlspec/adapters/psqlpy/driver.py +892 -0
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_type_handlers.py +90 -0
- sqlspec/adapters/psycopg/_types.py +18 -0
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +962 -0
- sqlspec/adapters/psycopg/config.py +487 -0
- sqlspec/adapters/psycopg/data_dictionary.py +630 -0
- sqlspec/adapters/psycopg/driver.py +1336 -0
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/spanner/__init__.py +38 -0
- sqlspec/adapters/spanner/_type_handlers.py +186 -0
- sqlspec/adapters/spanner/_types.py +12 -0
- sqlspec/adapters/spanner/adk/__init__.py +5 -0
- sqlspec/adapters/spanner/adk/store.py +435 -0
- sqlspec/adapters/spanner/config.py +241 -0
- sqlspec/adapters/spanner/data_dictionary.py +95 -0
- sqlspec/adapters/spanner/dialect/__init__.py +6 -0
- sqlspec/adapters/spanner/dialect/_spangres.py +52 -0
- sqlspec/adapters/spanner/dialect/_spanner.py +123 -0
- sqlspec/adapters/spanner/driver.py +366 -0
- sqlspec/adapters/spanner/litestar/__init__.py +5 -0
- sqlspec/adapters/spanner/litestar/store.py +266 -0
- sqlspec/adapters/spanner/type_converter.py +46 -0
- sqlspec/adapters/sqlite/__init__.py +18 -0
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +582 -0
- sqlspec/adapters/sqlite/config.py +221 -0
- sqlspec/adapters/sqlite/data_dictionary.py +256 -0
- sqlspec/adapters/sqlite/driver.py +527 -0
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +140 -0
- sqlspec/base.py +811 -0
- sqlspec/builder/__init__.py +146 -0
- sqlspec/builder/_base.py +900 -0
- sqlspec/builder/_column.py +517 -0
- sqlspec/builder/_ddl.py +1642 -0
- sqlspec/builder/_delete.py +84 -0
- sqlspec/builder/_dml.py +381 -0
- sqlspec/builder/_expression_wrappers.py +46 -0
- sqlspec/builder/_factory.py +1537 -0
- sqlspec/builder/_insert.py +315 -0
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +848 -0
- sqlspec/builder/_parsing_utils.py +297 -0
- sqlspec/builder/_select.py +1615 -0
- sqlspec/builder/_update.py +161 -0
- sqlspec/builder/_vector_expressions.py +259 -0
- sqlspec/cli.py +764 -0
- sqlspec/config.py +1540 -0
- sqlspec/core/__init__.py +305 -0
- sqlspec/core/cache.py +785 -0
- sqlspec/core/compiler.py +603 -0
- sqlspec/core/filters.py +872 -0
- sqlspec/core/hashing.py +274 -0
- sqlspec/core/metrics.py +83 -0
- sqlspec/core/parameters/__init__.py +64 -0
- sqlspec/core/parameters/_alignment.py +266 -0
- sqlspec/core/parameters/_converter.py +413 -0
- sqlspec/core/parameters/_processor.py +341 -0
- sqlspec/core/parameters/_registry.py +201 -0
- sqlspec/core/parameters/_transformers.py +226 -0
- sqlspec/core/parameters/_types.py +430 -0
- sqlspec/core/parameters/_validator.py +123 -0
- sqlspec/core/pipeline.py +187 -0
- sqlspec/core/result.py +1124 -0
- sqlspec/core/splitter.py +940 -0
- sqlspec/core/stack.py +163 -0
- sqlspec/core/statement.py +835 -0
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +36 -0
- sqlspec/driver/_async.py +1027 -0
- sqlspec/driver/_common.py +1236 -0
- sqlspec/driver/_sync.py +1025 -0
- sqlspec/driver/mixins/__init__.py +7 -0
- sqlspec/driver/mixins/_result_tools.py +61 -0
- sqlspec/driver/mixins/_sql_translator.py +122 -0
- sqlspec/driver/mixins/_storage.py +311 -0
- sqlspec/exceptions.py +321 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +471 -0
- sqlspec/extensions/fastapi/__init__.py +19 -0
- sqlspec/extensions/fastapi/extension.py +341 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +72 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +402 -0
- sqlspec/extensions/litestar/__init__.py +23 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +92 -0
- sqlspec/extensions/litestar/config.py +90 -0
- sqlspec/extensions/litestar/handlers.py +316 -0
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +638 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/otel/__init__.py +58 -0
- sqlspec/extensions/prometheus/__init__.py +107 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +26 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +257 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +716 -0
- sqlspec/migrations/__init__.py +36 -0
- sqlspec/migrations/base.py +728 -0
- sqlspec/migrations/commands.py +1140 -0
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +203 -0
- sqlspec/migrations/loaders.py +450 -0
- sqlspec/migrations/runner.py +1024 -0
- sqlspec/migrations/templates.py +234 -0
- sqlspec/migrations/tracker.py +403 -0
- sqlspec/migrations/utils.py +256 -0
- sqlspec/migrations/validation.py +203 -0
- sqlspec/observability/__init__.py +22 -0
- sqlspec/observability/_config.py +228 -0
- sqlspec/observability/_diagnostics.py +67 -0
- sqlspec/observability/_dispatcher.py +151 -0
- sqlspec/observability/_observer.py +180 -0
- sqlspec/observability/_runtime.py +381 -0
- sqlspec/observability/_spans.py +158 -0
- sqlspec/protocols.py +530 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +46 -0
- sqlspec/storage/_utils.py +104 -0
- sqlspec/storage/backends/__init__.py +1 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +398 -0
- sqlspec/storage/backends/local.py +377 -0
- sqlspec/storage/backends/obstore.py +580 -0
- sqlspec/storage/errors.py +104 -0
- sqlspec/storage/pipeline.py +604 -0
- sqlspec/storage/registry.py +289 -0
- sqlspec/typing.py +219 -0
- sqlspec/utils/__init__.py +31 -0
- sqlspec/utils/arrow_helpers.py +95 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +132 -0
- sqlspec/utils/data_transformation.py +114 -0
- sqlspec/utils/dependencies.py +79 -0
- sqlspec/utils/deprecation.py +113 -0
- sqlspec/utils/fixtures.py +250 -0
- sqlspec/utils/logging.py +172 -0
- sqlspec/utils/module_loader.py +273 -0
- sqlspec/utils/portal.py +325 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +396 -0
- sqlspec/utils/singleton.py +41 -0
- sqlspec/utils/sync_tools.py +277 -0
- sqlspec/utils/text.py +108 -0
- sqlspec/utils/type_converters.py +99 -0
- sqlspec/utils/type_guards.py +1324 -0
- sqlspec/utils/version.py +444 -0
- sqlspec-0.32.0.dist-info/METADATA +202 -0
- sqlspec-0.32.0.dist-info/RECORD +262 -0
- sqlspec-0.32.0.dist-info/WHEEL +4 -0
- sqlspec-0.32.0.dist-info/entry_points.txt +2 -0
- 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)
|