sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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.
- sqlspec/__init__.py +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +51 -186
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -115
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/driver/_common.py
CHANGED
|
@@ -1,373 +1,631 @@
|
|
|
1
|
-
"""Common driver attributes and utilities.
|
|
1
|
+
"""Common driver attributes and utilities.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Optional
|
|
3
|
+
This module provides core driver infrastructure including execution result handling,
|
|
4
|
+
common driver attributes, parameter processing, and SQL compilation utilities.
|
|
5
|
+
"""
|
|
7
6
|
|
|
8
|
-
import
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, Union, cast
|
|
8
|
+
|
|
9
|
+
from mypy_extensions import trait
|
|
9
10
|
from sqlglot import exp
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from sqlspec.
|
|
13
|
-
from sqlspec.
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.
|
|
16
|
-
from sqlspec.statement.splitter import split_sql_script
|
|
17
|
-
from sqlspec.typing import ConnectionT, DictRow, RowT, T
|
|
11
|
+
|
|
12
|
+
from sqlspec.builder import QueryBuilder
|
|
13
|
+
from sqlspec.core import SQL, OperationType, ParameterStyle, SQLResult, Statement, StatementConfig, TypedParameter
|
|
14
|
+
from sqlspec.core.cache import get_cache_config, sql_cache
|
|
15
|
+
from sqlspec.core.splitter import split_sql_script
|
|
16
|
+
from sqlspec.exceptions import ImproperConfigurationError
|
|
18
17
|
from sqlspec.utils.logging import get_logger
|
|
19
18
|
|
|
20
19
|
if TYPE_CHECKING:
|
|
21
|
-
from
|
|
22
|
-
|
|
20
|
+
from collections.abc import Sequence
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
from sqlspec.core.filters import FilterTypeT, StatementFilter
|
|
23
|
+
from sqlspec.typing import StatementParameters
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
__all__ = (
|
|
27
|
+
"DEFAULT_EXECUTION_RESULT",
|
|
28
|
+
"EXEC_CURSOR_RESULT",
|
|
29
|
+
"EXEC_ROWCOUNT_OVERRIDE",
|
|
30
|
+
"EXEC_SPECIAL_DATA",
|
|
31
|
+
"CommonDriverAttributesMixin",
|
|
32
|
+
"ExecutionResult",
|
|
33
|
+
"ScriptExecutionResult",
|
|
34
|
+
)
|
|
28
35
|
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
"""Common attributes and methods for driver adapters."""
|
|
37
|
+
logger = get_logger("driver")
|
|
32
38
|
|
|
33
|
-
__slots__ = ("config", "connection", "default_row_type")
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""
|
|
40
|
+
class ScriptExecutionResult(NamedTuple):
|
|
41
|
+
"""Result from script execution with statement count information.
|
|
42
|
+
|
|
43
|
+
This named tuple eliminates the need for redundant script splitting
|
|
44
|
+
by providing statement count information during execution rather than
|
|
45
|
+
requiring re-parsing after execution.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
cursor_result: The result returned by the database cursor/driver
|
|
49
|
+
rowcount_override: Optional override for the number of affected rows
|
|
50
|
+
special_data: Any special metadata or additional information
|
|
51
|
+
statement_count: Total number of statements in the script
|
|
52
|
+
successful_statements: Number of statements that executed successfully
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
cursor_result: Any
|
|
56
|
+
rowcount_override: Optional[int]
|
|
57
|
+
special_data: Any
|
|
58
|
+
statement_count: int
|
|
59
|
+
successful_statements: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ExecutionResult(NamedTuple):
|
|
63
|
+
"""Comprehensive execution result containing all data needed for SQLResult building.
|
|
64
|
+
|
|
65
|
+
This named tuple consolidates all execution result data to eliminate the need
|
|
66
|
+
for additional data extraction calls and script re-parsing in build_statement_result.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
cursor_result: The raw result returned by the database cursor/driver
|
|
70
|
+
rowcount_override: Optional override for the number of affected rows
|
|
71
|
+
special_data: Any special metadata or additional information from execution
|
|
72
|
+
selected_data: For SELECT operations, the extracted row data
|
|
73
|
+
column_names: For SELECT operations, the column names
|
|
74
|
+
data_row_count: For SELECT operations, the number of rows returned
|
|
75
|
+
statement_count: For script operations, total number of statements
|
|
76
|
+
successful_statements: For script operations, number of successful statements
|
|
77
|
+
is_script_result: Whether this result is from script execution
|
|
78
|
+
is_select_result: Whether this result is from a SELECT operation
|
|
79
|
+
is_many_result: Whether this result is from an execute_many operation
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
cursor_result: Any
|
|
83
|
+
rowcount_override: Optional[int]
|
|
84
|
+
special_data: Any
|
|
85
|
+
selected_data: Optional["list[dict[str, Any]]"]
|
|
86
|
+
column_names: Optional["list[str]"]
|
|
87
|
+
data_row_count: Optional[int]
|
|
88
|
+
statement_count: Optional[int]
|
|
89
|
+
successful_statements: Optional[int]
|
|
90
|
+
is_script_result: bool
|
|
91
|
+
is_select_result: bool
|
|
92
|
+
is_many_result: bool
|
|
93
|
+
last_inserted_id: Optional[Union[int, str]] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
EXEC_CURSOR_RESULT = 0
|
|
97
|
+
EXEC_ROWCOUNT_OVERRIDE = 1
|
|
98
|
+
EXEC_SPECIAL_DATA = 2
|
|
99
|
+
DEFAULT_EXECUTION_RESULT: Final[tuple[Any, Optional[int], Any]] = (None, None, None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@trait
|
|
103
|
+
class CommonDriverAttributesMixin:
|
|
104
|
+
"""Common attributes and methods for driver adapters.
|
|
105
|
+
|
|
106
|
+
This mixin provides the foundation for all SQLSpec drivers, including
|
|
107
|
+
connection and configuration management, parameter processing, caching,
|
|
108
|
+
and SQL compilation.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
__slots__ = ("connection", "driver_features", "statement_config")
|
|
112
|
+
connection: "Any"
|
|
113
|
+
statement_config: "StatementConfig"
|
|
114
|
+
driver_features: "dict[str, Any]"
|
|
49
115
|
|
|
50
116
|
def __init__(
|
|
51
|
-
self,
|
|
52
|
-
connection: "ConnectionT",
|
|
53
|
-
config: "Optional[SQLConfig]" = None,
|
|
54
|
-
default_row_type: "type[DictRow]" = dict[str, Any],
|
|
117
|
+
self, connection: "Any", statement_config: "StatementConfig", driver_features: "Optional[dict[str, Any]]" = None
|
|
55
118
|
) -> None:
|
|
56
|
-
"""Initialize with connection
|
|
119
|
+
"""Initialize driver adapter with connection and configuration.
|
|
57
120
|
|
|
58
121
|
Args:
|
|
59
|
-
connection:
|
|
60
|
-
|
|
61
|
-
|
|
122
|
+
connection: Database connection instance
|
|
123
|
+
statement_config: Statement configuration for the driver
|
|
124
|
+
driver_features: Driver-specific features like extensions, secrets, and connection callbacks
|
|
62
125
|
"""
|
|
63
126
|
self.connection = connection
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
127
|
+
self.statement_config = statement_config
|
|
128
|
+
self.driver_features = driver_features or {}
|
|
66
129
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
def create_execution_result(
|
|
131
|
+
self,
|
|
132
|
+
cursor_result: Any,
|
|
133
|
+
*,
|
|
134
|
+
rowcount_override: Optional[int] = None,
|
|
135
|
+
special_data: Any = None,
|
|
136
|
+
selected_data: Optional["list[dict[str, Any]]"] = None,
|
|
137
|
+
column_names: Optional["list[str]"] = None,
|
|
138
|
+
data_row_count: Optional[int] = None,
|
|
139
|
+
statement_count: Optional[int] = None,
|
|
140
|
+
successful_statements: Optional[int] = None,
|
|
141
|
+
is_script_result: bool = False,
|
|
142
|
+
is_select_result: bool = False,
|
|
143
|
+
is_many_result: bool = False,
|
|
144
|
+
last_inserted_id: Optional[Union[int, str]] = None,
|
|
145
|
+
) -> ExecutionResult:
|
|
146
|
+
"""Create ExecutionResult with all necessary data for any operation type.
|
|
72
147
|
|
|
73
148
|
Args:
|
|
74
|
-
|
|
149
|
+
cursor_result: The raw result returned by the database cursor/driver
|
|
150
|
+
rowcount_override: Optional override for the number of affected rows
|
|
151
|
+
special_data: Any special metadata or additional information
|
|
152
|
+
selected_data: For SELECT operations, the extracted row data
|
|
153
|
+
column_names: For SELECT operations, the column names
|
|
154
|
+
data_row_count: For SELECT operations, the number of rows returned
|
|
155
|
+
statement_count: For script operations, total number of statements
|
|
156
|
+
successful_statements: For script operations, number of successful statements
|
|
157
|
+
is_script_result: Whether this result is from script execution
|
|
158
|
+
is_select_result: Whether this result is from a SELECT operation
|
|
159
|
+
is_many_result: Whether this result is from an execute_many operation
|
|
160
|
+
last_inserted_id: The ID of the last inserted row (if applicable)
|
|
75
161
|
|
|
76
162
|
Returns:
|
|
77
|
-
|
|
78
|
-
that is not a CTE definition.
|
|
163
|
+
ExecutionResult configured for the specified operation type
|
|
79
164
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
165
|
+
return ExecutionResult(
|
|
166
|
+
cursor_result=cursor_result,
|
|
167
|
+
rowcount_override=rowcount_override,
|
|
168
|
+
special_data=special_data,
|
|
169
|
+
selected_data=selected_data,
|
|
170
|
+
column_names=column_names,
|
|
171
|
+
data_row_count=data_row_count,
|
|
172
|
+
statement_count=statement_count,
|
|
173
|
+
successful_statements=successful_statements,
|
|
174
|
+
is_script_result=is_script_result,
|
|
175
|
+
is_select_result=is_select_result,
|
|
176
|
+
is_many_result=is_many_result,
|
|
177
|
+
last_inserted_id=last_inserted_id,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def build_statement_result(self, statement: "SQL", execution_result: ExecutionResult) -> "SQLResult":
|
|
181
|
+
"""Build and return the SQLResult from ExecutionResult data.
|
|
182
|
+
|
|
183
|
+
Creates SQLResult objects from ExecutionResult data without requiring
|
|
184
|
+
additional data extraction calls or script re-parsing.
|
|
99
185
|
|
|
100
186
|
Args:
|
|
101
|
-
|
|
187
|
+
statement: SQL statement that was executed
|
|
188
|
+
execution_result: ExecutionResult containing all necessary data
|
|
102
189
|
|
|
103
190
|
Returns:
|
|
104
|
-
|
|
191
|
+
SQLResult with complete execution data
|
|
105
192
|
"""
|
|
193
|
+
if execution_result.is_script_result:
|
|
194
|
+
return SQLResult(
|
|
195
|
+
statement=statement,
|
|
196
|
+
data=[],
|
|
197
|
+
rows_affected=execution_result.rowcount_override or 0,
|
|
198
|
+
operation_type="SCRIPT",
|
|
199
|
+
total_statements=execution_result.statement_count or 0,
|
|
200
|
+
successful_statements=execution_result.successful_statements or 0,
|
|
201
|
+
metadata=execution_result.special_data or {"status_message": "OK"},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if execution_result.is_select_result:
|
|
205
|
+
return SQLResult(
|
|
206
|
+
statement=statement,
|
|
207
|
+
data=execution_result.selected_data or [],
|
|
208
|
+
column_names=execution_result.column_names or [],
|
|
209
|
+
rows_affected=execution_result.data_row_count or 0,
|
|
210
|
+
operation_type="SELECT",
|
|
211
|
+
metadata=execution_result.special_data or {},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return SQLResult(
|
|
215
|
+
statement=statement,
|
|
216
|
+
data=[],
|
|
217
|
+
rows_affected=execution_result.rowcount_override or 0,
|
|
218
|
+
operation_type=self._determine_operation_type(statement),
|
|
219
|
+
last_inserted_id=execution_result.last_inserted_id,
|
|
220
|
+
metadata=execution_result.special_data or {"status_message": "OK"},
|
|
221
|
+
)
|
|
106
222
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return False
|
|
223
|
+
def _determine_operation_type(self, statement: "Any") -> OperationType:
|
|
224
|
+
"""Determine operation type from SQL statement expression.
|
|
110
225
|
|
|
111
|
-
|
|
112
|
-
|
|
226
|
+
Examines the statement's expression type to determine if it's
|
|
227
|
+
INSERT, UPDATE, DELETE, SELECT, SCRIPT, or generic EXECUTE.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
statement: SQL statement object with expression attribute
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
OperationType literal value
|
|
234
|
+
"""
|
|
235
|
+
if statement.is_script:
|
|
236
|
+
return "SCRIPT"
|
|
113
237
|
|
|
114
|
-
# Approach 1: Try to re-parse with placeholders replaced
|
|
115
|
-
try:
|
|
116
|
-
sanitized_sql = placeholder_regex.sub("1", sql_text)
|
|
117
|
-
|
|
118
|
-
# If we replaced any placeholders, try parsing again
|
|
119
|
-
if sanitized_sql != sql_text:
|
|
120
|
-
parsed = sqlglot.parse_one(sanitized_sql, read=None)
|
|
121
|
-
if isinstance(
|
|
122
|
-
parsed, (exp.Select, exp.Values, exp.Table, exp.Show, exp.Describe, exp.Pragma, exp.Command)
|
|
123
|
-
):
|
|
124
|
-
return True
|
|
125
|
-
if isinstance(parsed, exp.With) and parsed.expressions:
|
|
126
|
-
return self.returns_rows(parsed.expressions[-1])
|
|
127
|
-
if isinstance(parsed, (exp.Insert, exp.Update, exp.Delete)):
|
|
128
|
-
return bool(parsed.find(exp.Returning))
|
|
129
|
-
if not isinstance(parsed, exp.Anonymous):
|
|
130
|
-
return False
|
|
131
|
-
except Exception:
|
|
132
|
-
logger.debug("Could not parse using placeholders. Using tokenizer. %s", sql_text)
|
|
133
|
-
|
|
134
|
-
# Approach 2: Use tokenizer for robust keyword detection
|
|
135
238
|
try:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
239
|
+
expression = statement.expression
|
|
240
|
+
except AttributeError:
|
|
241
|
+
return "EXECUTE"
|
|
242
|
+
|
|
243
|
+
if not expression:
|
|
244
|
+
return "EXECUTE"
|
|
245
|
+
|
|
246
|
+
expr_type = type(expression).__name__.upper()
|
|
247
|
+
|
|
248
|
+
if "ANONYMOUS" in expr_type and statement.is_script:
|
|
249
|
+
return "SCRIPT"
|
|
250
|
+
|
|
251
|
+
if "INSERT" in expr_type:
|
|
252
|
+
return "INSERT"
|
|
253
|
+
if "UPDATE" in expr_type:
|
|
254
|
+
return "UPDATE"
|
|
255
|
+
if "DELETE" in expr_type:
|
|
256
|
+
return "DELETE"
|
|
257
|
+
if "SELECT" in expr_type:
|
|
258
|
+
return "SELECT"
|
|
259
|
+
if "COPY" in expr_type:
|
|
260
|
+
return "COPY"
|
|
261
|
+
return "EXECUTE"
|
|
262
|
+
|
|
263
|
+
def prepare_statement(
|
|
264
|
+
self,
|
|
265
|
+
statement: "Union[Statement, QueryBuilder]",
|
|
266
|
+
parameters: "tuple[Union[StatementParameters, StatementFilter], ...]" = (),
|
|
267
|
+
*,
|
|
268
|
+
statement_config: "StatementConfig",
|
|
269
|
+
kwargs: "Optional[dict[str, Any]]" = None,
|
|
270
|
+
) -> "SQL":
|
|
271
|
+
"""Build SQL statement from various input types.
|
|
272
|
+
|
|
273
|
+
Ensures dialect is set and preserves existing state when rebuilding SQL objects.
|
|
274
|
+
"""
|
|
275
|
+
kwargs = kwargs or {}
|
|
276
|
+
|
|
277
|
+
if isinstance(statement, QueryBuilder):
|
|
278
|
+
return statement.to_statement(statement_config)
|
|
279
|
+
if isinstance(statement, SQL):
|
|
280
|
+
if parameters or kwargs:
|
|
281
|
+
merged_parameters = (
|
|
282
|
+
(*statement._positional_parameters, *parameters) if parameters else statement._positional_parameters
|
|
283
|
+
)
|
|
284
|
+
return SQL(statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
|
|
285
|
+
needs_rebuild = False
|
|
286
|
+
|
|
287
|
+
if statement_config.dialect and (
|
|
288
|
+
not statement.statement_config.dialect or statement.statement_config.dialect != statement_config.dialect
|
|
289
|
+
):
|
|
290
|
+
needs_rebuild = True
|
|
155
291
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
292
|
+
if (
|
|
293
|
+
statement.statement_config.parameter_config.default_execution_parameter_style
|
|
294
|
+
!= statement_config.parameter_config.default_execution_parameter_style
|
|
295
|
+
):
|
|
296
|
+
needs_rebuild = True
|
|
159
297
|
|
|
160
|
-
|
|
161
|
-
|
|
298
|
+
if needs_rebuild:
|
|
299
|
+
sql_text = statement._raw_sql or statement.sql
|
|
300
|
+
|
|
301
|
+
if statement.is_many and statement.parameters:
|
|
302
|
+
new_sql = SQL(sql_text, statement.parameters, statement_config=statement_config, is_many=True)
|
|
303
|
+
elif statement._named_parameters:
|
|
304
|
+
new_sql = SQL(sql_text, statement_config=statement_config, **statement._named_parameters)
|
|
305
|
+
else:
|
|
306
|
+
new_sql = SQL(sql_text, *statement._positional_parameters, statement_config=statement_config)
|
|
307
|
+
|
|
308
|
+
return new_sql
|
|
309
|
+
return statement
|
|
310
|
+
return SQL(statement, *parameters, statement_config=statement_config, **kwargs)
|
|
311
|
+
|
|
312
|
+
def split_script_statements(
|
|
313
|
+
self, script: str, statement_config: "StatementConfig", strip_trailing_semicolon: bool = False
|
|
314
|
+
) -> list[str]:
|
|
315
|
+
"""Split a SQL script into individual statements.
|
|
162
316
|
|
|
163
|
-
|
|
164
|
-
|
|
317
|
+
Uses a lexer-driven state machine to handle multi-statement scripts,
|
|
318
|
+
including complex constructs like PL/SQL blocks, T-SQL batches, and nested blocks.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
script: The SQL script to split
|
|
322
|
+
statement_config: Statement configuration containing dialect information
|
|
323
|
+
strip_trailing_semicolon: If True, remove trailing semicolons from statements
|
|
165
324
|
|
|
166
325
|
Returns:
|
|
167
|
-
|
|
326
|
+
A list of individual SQL statements
|
|
168
327
|
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
328
|
+
return [
|
|
329
|
+
sql_script.strip()
|
|
330
|
+
for sql_script in split_sql_script(
|
|
331
|
+
script, dialect=str(statement_config.dialect), strip_trailing_terminator=strip_trailing_semicolon
|
|
332
|
+
)
|
|
333
|
+
if sql_script.strip()
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
def prepare_driver_parameters(
|
|
337
|
+
self, parameters: Any, statement_config: "StatementConfig", is_many: bool = False
|
|
176
338
|
) -> Any:
|
|
177
|
-
"""
|
|
339
|
+
"""Prepare parameters for database driver consumption.
|
|
178
340
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
and what the driver expects.
|
|
341
|
+
Normalizes parameter structure and unwraps TypedParameter objects
|
|
342
|
+
to their underlying values, which database drivers expect.
|
|
182
343
|
|
|
183
344
|
Args:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
345
|
+
parameters: Parameters in any format (dict, list, tuple, scalar, TypedParameter)
|
|
346
|
+
statement_config: Statement configuration for parameter style detection
|
|
347
|
+
is_many: If True, handle as executemany parameter sequence
|
|
187
348
|
|
|
188
349
|
Returns:
|
|
189
|
-
Parameters
|
|
350
|
+
Parameters with TypedParameter objects unwrapped to primitive values
|
|
190
351
|
"""
|
|
191
|
-
if parameters is None:
|
|
352
|
+
if parameters is None and statement_config.parameter_config.needs_static_script_compilation:
|
|
192
353
|
return None
|
|
193
354
|
|
|
194
|
-
|
|
195
|
-
|
|
355
|
+
if not parameters:
|
|
356
|
+
return []
|
|
196
357
|
|
|
197
|
-
if
|
|
198
|
-
|
|
358
|
+
if is_many:
|
|
359
|
+
if isinstance(parameters, list):
|
|
360
|
+
return [self._format_parameter_set_for_many(param_set, statement_config) for param_set in parameters]
|
|
361
|
+
return [self._format_parameter_set_for_many(parameters, statement_config)]
|
|
362
|
+
return self._format_parameter_set(parameters, statement_config)
|
|
199
363
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
actual_styles = {p.style for p in param_info_list if p.style}
|
|
204
|
-
if len(actual_styles) == 1:
|
|
205
|
-
detected_style = actual_styles.pop()
|
|
206
|
-
if detected_style != target_style:
|
|
207
|
-
target_style = detected_style
|
|
208
|
-
|
|
209
|
-
# Analyze what format the driver expects based on the placeholder style
|
|
210
|
-
driver_expects_dict = target_style in {
|
|
211
|
-
ParameterStyle.NAMED_COLON,
|
|
212
|
-
ParameterStyle.POSITIONAL_COLON,
|
|
213
|
-
ParameterStyle.NAMED_AT,
|
|
214
|
-
ParameterStyle.NAMED_DOLLAR,
|
|
215
|
-
ParameterStyle.NAMED_PYFORMAT,
|
|
216
|
-
}
|
|
364
|
+
def _format_parameter_set_for_many(self, parameters: Any, statement_config: "StatementConfig") -> Any:
|
|
365
|
+
"""Prepare a single parameter set for execute_many operations.
|
|
217
366
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
parameters, (str, bytes)
|
|
221
|
-
)
|
|
367
|
+
Unlike _format_parameter_set, this method handles parameter sets without
|
|
368
|
+
converting the structure itself to array format.
|
|
222
369
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if driver_expects_dict and params_are_dict:
|
|
233
|
-
if target_style == ParameterStyle.POSITIONAL_COLON and all(
|
|
234
|
-
p.name and p.name.isdigit() for p in param_info_list
|
|
235
|
-
):
|
|
236
|
-
# If all parameters are numeric but named, convert to dict
|
|
237
|
-
# SQL has numeric placeholders but params might have named keys
|
|
238
|
-
numeric_keys_expected = {p.name for p in param_info_list if p.name}
|
|
239
|
-
if not numeric_keys_expected.issubset(parameters.keys()):
|
|
240
|
-
# Need to convert named keys to numeric positions
|
|
241
|
-
numeric_result: dict[str, Any] = {}
|
|
242
|
-
param_values = list(parameters.values())
|
|
243
|
-
for param_info in param_info_list:
|
|
244
|
-
if param_info.name and param_info.ordinal < len(param_values):
|
|
245
|
-
numeric_result[param_info.name] = param_values[param_info.ordinal]
|
|
246
|
-
return numeric_result
|
|
247
|
-
|
|
248
|
-
# Special case: Auto-generated param_N style when SQL expects specific names
|
|
249
|
-
if all(key.startswith("param_") and key[6:].isdigit() for key in parameters):
|
|
250
|
-
sql_param_names = {p.name for p in param_info_list if p.name}
|
|
251
|
-
if sql_param_names and not any(name.startswith("param_") for name in sql_param_names):
|
|
252
|
-
# SQL has specific names, not param_N style - don't use these params as-is
|
|
253
|
-
# This likely indicates a mismatch in parameter generation
|
|
254
|
-
# For now, pass through and let validation catch it
|
|
255
|
-
pass
|
|
256
|
-
|
|
257
|
-
return parameters
|
|
258
|
-
|
|
259
|
-
if not driver_expects_dict and params_are_sequence:
|
|
260
|
-
# Formats match - return as-is
|
|
261
|
-
return parameters
|
|
262
|
-
|
|
263
|
-
# Formats don't match - need conversion
|
|
264
|
-
if driver_expects_dict and params_are_sequence:
|
|
265
|
-
dict_result: dict[str, Any] = {}
|
|
266
|
-
for i, (param_info, value) in enumerate(zip(param_info_list, parameters)):
|
|
267
|
-
if param_info.name:
|
|
268
|
-
if param_info.style == ParameterStyle.POSITIONAL_COLON and param_info.name.isdigit():
|
|
269
|
-
# Oracle uses string keys even for numeric placeholders
|
|
270
|
-
dict_result[param_info.name] = value
|
|
271
|
-
else:
|
|
272
|
-
dict_result[param_info.name] = value
|
|
273
|
-
else:
|
|
274
|
-
# Use param_N format for unnamed placeholders
|
|
275
|
-
dict_result[f"param_{i}"] = value
|
|
276
|
-
return dict_result
|
|
277
|
-
|
|
278
|
-
if not driver_expects_dict and params_are_dict:
|
|
279
|
-
# First check if it's already in param_N format
|
|
280
|
-
if all(key.startswith("param_") and key[6:].isdigit() for key in parameters):
|
|
281
|
-
positional_result: list[Any] = []
|
|
282
|
-
for i in range(len(param_info_list)):
|
|
283
|
-
key = f"param_{i}"
|
|
284
|
-
if key in parameters:
|
|
285
|
-
positional_result.append(parameters[key])
|
|
286
|
-
return positional_result
|
|
287
|
-
|
|
288
|
-
positional_params: list[Any] = []
|
|
289
|
-
for param_info in param_info_list:
|
|
290
|
-
if param_info.name and param_info.name in parameters:
|
|
291
|
-
positional_params.append(parameters[param_info.name])
|
|
292
|
-
elif f"param_{param_info.ordinal}" in parameters:
|
|
293
|
-
positional_params.append(parameters[f"param_{param_info.ordinal}"])
|
|
294
|
-
else:
|
|
295
|
-
# Try to match by position if we have a simple dict
|
|
296
|
-
param_values = list(parameters.values())
|
|
297
|
-
if param_info.ordinal < len(param_values):
|
|
298
|
-
positional_params.append(param_values[param_info.ordinal])
|
|
299
|
-
return positional_params or list(parameters.values())
|
|
370
|
+
Args:
|
|
371
|
+
parameters: Single parameter set (tuple, list, or dict)
|
|
372
|
+
statement_config: Statement configuration for parameter style detection
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Processed parameter set with individual values coerced but structure preserved
|
|
376
|
+
"""
|
|
377
|
+
if not parameters:
|
|
378
|
+
return []
|
|
300
379
|
|
|
301
|
-
|
|
302
|
-
|
|
380
|
+
def apply_type_coercion(value: Any) -> Any:
|
|
381
|
+
"""Apply type coercion to a single value."""
|
|
382
|
+
unwrapped_value = value.value if isinstance(value, TypedParameter) else value
|
|
303
383
|
|
|
304
|
-
|
|
305
|
-
|
|
384
|
+
if statement_config.parameter_config.type_coercion_map:
|
|
385
|
+
for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
|
|
386
|
+
if type_check in {list, tuple} and isinstance(unwrapped_value, (list, tuple)):
|
|
387
|
+
continue
|
|
388
|
+
if isinstance(unwrapped_value, type_check):
|
|
389
|
+
return converter(unwrapped_value)
|
|
390
|
+
|
|
391
|
+
return unwrapped_value
|
|
392
|
+
|
|
393
|
+
if isinstance(parameters, dict):
|
|
394
|
+
return {k: apply_type_coercion(v) for k, v in parameters.items()}
|
|
395
|
+
|
|
396
|
+
if isinstance(parameters, (list, tuple)):
|
|
397
|
+
coerced_params = [apply_type_coercion(p) for p in parameters]
|
|
398
|
+
return tuple(coerced_params) if isinstance(parameters, tuple) else coerced_params
|
|
399
|
+
|
|
400
|
+
return apply_type_coercion(parameters)
|
|
306
401
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
PL/SQL blocks, T-SQL batches, and nested blocks.
|
|
402
|
+
def _format_parameter_set(self, parameters: Any, statement_config: "StatementConfig") -> Any:
|
|
403
|
+
"""Prepare a single parameter set for database driver consumption.
|
|
310
404
|
|
|
311
405
|
Args:
|
|
312
|
-
|
|
313
|
-
|
|
406
|
+
parameters: Single parameter set in any format
|
|
407
|
+
statement_config: Statement configuration for parameter style detection
|
|
314
408
|
|
|
315
409
|
Returns:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
Note:
|
|
319
|
-
This is particularly useful for databases that don't natively
|
|
320
|
-
support multi-statement execution (e.g., Oracle, some async drivers).
|
|
410
|
+
Processed parameter set with TypedParameter objects unwrapped and type coercion applied
|
|
321
411
|
"""
|
|
322
|
-
|
|
323
|
-
|
|
412
|
+
if not parameters:
|
|
413
|
+
return []
|
|
324
414
|
|
|
325
|
-
|
|
326
|
-
|
|
415
|
+
def apply_type_coercion(value: Any) -> Any:
|
|
416
|
+
"""Apply type coercion to a single value."""
|
|
417
|
+
unwrapped_value = value.value if isinstance(value, TypedParameter) else value
|
|
327
418
|
|
|
328
|
-
|
|
329
|
-
|
|
419
|
+
if statement_config.parameter_config.type_coercion_map:
|
|
420
|
+
for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
|
|
421
|
+
if isinstance(unwrapped_value, type_check):
|
|
422
|
+
return converter(unwrapped_value)
|
|
423
|
+
|
|
424
|
+
return unwrapped_value
|
|
425
|
+
|
|
426
|
+
if isinstance(parameters, dict):
|
|
427
|
+
if not parameters:
|
|
428
|
+
return []
|
|
429
|
+
if statement_config.parameter_config.supported_execution_parameter_styles and (
|
|
430
|
+
ParameterStyle.NAMED_PYFORMAT in statement_config.parameter_config.supported_execution_parameter_styles
|
|
431
|
+
or ParameterStyle.NAMED_COLON in statement_config.parameter_config.supported_execution_parameter_styles
|
|
432
|
+
):
|
|
433
|
+
return {k: apply_type_coercion(v) for k, v in parameters.items()}
|
|
434
|
+
if statement_config.parameter_config.default_parameter_style in {
|
|
435
|
+
ParameterStyle.NUMERIC,
|
|
436
|
+
ParameterStyle.QMARK,
|
|
437
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
438
|
+
}:
|
|
439
|
+
ordered_parameters = []
|
|
440
|
+
sorted_items = sorted(
|
|
441
|
+
parameters.items(),
|
|
442
|
+
key=lambda item: int(item[0])
|
|
443
|
+
if item[0].isdigit()
|
|
444
|
+
else (int(item[0][6:]) if item[0].startswith("param_") and item[0][6:].isdigit() else float("inf")),
|
|
445
|
+
)
|
|
446
|
+
for _, value in sorted_items:
|
|
447
|
+
ordered_parameters.append(apply_type_coercion(value))
|
|
448
|
+
return ordered_parameters
|
|
449
|
+
|
|
450
|
+
return {k: apply_type_coercion(v) for k, v in parameters.items()}
|
|
451
|
+
|
|
452
|
+
if isinstance(parameters, (list, tuple)):
|
|
453
|
+
coerced_params = [apply_type_coercion(p) for p in parameters]
|
|
454
|
+
if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
|
|
455
|
+
return tuple(coerced_params)
|
|
456
|
+
return coerced_params
|
|
457
|
+
|
|
458
|
+
return [apply_type_coercion(parameters)]
|
|
459
|
+
|
|
460
|
+
def _get_compiled_sql(
|
|
461
|
+
self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
|
|
462
|
+
) -> tuple[str, Any]:
|
|
463
|
+
"""Get compiled SQL with parameter style conversion and caching.
|
|
464
|
+
|
|
465
|
+
Compiles the SQL statement and applies parameter style conversion.
|
|
466
|
+
Results are cached when caching is enabled.
|
|
330
467
|
|
|
331
468
|
Args:
|
|
332
|
-
|
|
469
|
+
statement: SQL statement to compile
|
|
470
|
+
statement_config: Complete statement configuration including parameter config, dialect, etc.
|
|
471
|
+
flatten_single_parameters: If True, flatten single-element lists for scalar parameters
|
|
333
472
|
|
|
334
473
|
Returns:
|
|
335
|
-
|
|
474
|
+
Tuple of (compiled_sql, parameters)
|
|
336
475
|
"""
|
|
476
|
+
cache_config = get_cache_config()
|
|
477
|
+
cache_key = None
|
|
478
|
+
if cache_config.compiled_cache_enabled and statement_config.enable_caching:
|
|
479
|
+
cache_key = self._generate_compilation_cache_key(statement, statement_config, flatten_single_parameters)
|
|
480
|
+
cached_result = sql_cache.get(cache_key)
|
|
481
|
+
if cached_result is not None:
|
|
482
|
+
return cached_result
|
|
483
|
+
|
|
484
|
+
compiled_sql, execution_parameters = statement.compile()
|
|
485
|
+
|
|
486
|
+
prepared_parameters = self.prepare_driver_parameters(
|
|
487
|
+
execution_parameters, statement_config, is_many=statement.is_many
|
|
488
|
+
)
|
|
337
489
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
490
|
+
if statement_config.parameter_config.output_transformer:
|
|
491
|
+
compiled_sql, prepared_parameters = statement_config.parameter_config.output_transformer(
|
|
492
|
+
compiled_sql, prepared_parameters
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if cache_key is not None:
|
|
496
|
+
sql_cache.set(cache_key, (compiled_sql, prepared_parameters))
|
|
497
|
+
|
|
498
|
+
return compiled_sql, prepared_parameters
|
|
499
|
+
|
|
500
|
+
def _generate_compilation_cache_key(
|
|
501
|
+
self, statement: "SQL", config: "StatementConfig", flatten_single_parameters: bool
|
|
502
|
+
) -> str:
|
|
503
|
+
"""Generate cache key that includes all compilation context.
|
|
504
|
+
|
|
505
|
+
Creates a deterministic cache key that includes all factors that affect SQL compilation,
|
|
506
|
+
preventing cache contamination between different compilation contexts.
|
|
507
|
+
"""
|
|
508
|
+
context_hash = hash(
|
|
509
|
+
(
|
|
510
|
+
config.parameter_config.hash(),
|
|
511
|
+
config.dialect,
|
|
512
|
+
statement.is_script,
|
|
513
|
+
statement.is_many,
|
|
514
|
+
flatten_single_parameters,
|
|
515
|
+
bool(config.parameter_config.output_transformer),
|
|
516
|
+
bool(config.parameter_config.ast_transformer),
|
|
517
|
+
bool(config.parameter_config.needs_static_script_compilation),
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
params = statement.parameters
|
|
522
|
+
params_key: Any
|
|
523
|
+
|
|
524
|
+
def make_hashable(obj: Any) -> Any:
|
|
525
|
+
"""Recursively convert unhashable types to hashable ones."""
|
|
526
|
+
if isinstance(obj, (list, tuple)):
|
|
527
|
+
return tuple(make_hashable(item) for item in obj)
|
|
528
|
+
if isinstance(obj, dict):
|
|
529
|
+
return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
|
|
530
|
+
if isinstance(obj, set):
|
|
531
|
+
return frozenset(make_hashable(item) for item in obj)
|
|
532
|
+
return obj
|
|
341
533
|
|
|
342
|
-
|
|
534
|
+
try:
|
|
535
|
+
if isinstance(params, dict):
|
|
536
|
+
params_key = make_hashable(params)
|
|
537
|
+
elif isinstance(params, (list, tuple)) and params:
|
|
538
|
+
if isinstance(params[0], dict):
|
|
539
|
+
params_key = tuple(make_hashable(d) for d in params)
|
|
540
|
+
else:
|
|
541
|
+
params_key = make_hashable(params)
|
|
542
|
+
elif isinstance(params, (list, tuple)):
|
|
543
|
+
params_key = ()
|
|
544
|
+
else:
|
|
545
|
+
params_key = params
|
|
546
|
+
except (TypeError, AttributeError):
|
|
547
|
+
params_key = str(params)
|
|
343
548
|
|
|
344
|
-
|
|
345
|
-
""
|
|
549
|
+
base_hash = hash((statement.sql, params_key, statement.is_many, statement.is_script))
|
|
550
|
+
return f"compiled:{base_hash}:{context_hash}"
|
|
346
551
|
|
|
347
|
-
|
|
348
|
-
|
|
552
|
+
def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "Optional[ParameterStyle]":
|
|
553
|
+
"""Determine the dominant parameter style from parameter info list.
|
|
349
554
|
|
|
350
555
|
Args:
|
|
351
|
-
parameters:
|
|
556
|
+
parameters: List of ParameterInfo objects from validator.extract_parameters()
|
|
352
557
|
|
|
353
558
|
Returns:
|
|
354
|
-
|
|
559
|
+
The dominant parameter style, or None if no parameters
|
|
355
560
|
"""
|
|
356
561
|
if not parameters:
|
|
357
|
-
return
|
|
358
|
-
return [self._prepare_driver_parameters(param_set) for param_set in parameters]
|
|
562
|
+
return None
|
|
359
563
|
|
|
360
|
-
|
|
361
|
-
|
|
564
|
+
style_counts: dict[ParameterStyle, int] = {}
|
|
565
|
+
for param in parameters:
|
|
566
|
+
style_counts[param.style] = style_counts.get(param.style, 0) + 1
|
|
567
|
+
|
|
568
|
+
precedence = {
|
|
569
|
+
ParameterStyle.QMARK: 1,
|
|
570
|
+
ParameterStyle.NUMERIC: 2,
|
|
571
|
+
ParameterStyle.POSITIONAL_COLON: 3,
|
|
572
|
+
ParameterStyle.POSITIONAL_PYFORMAT: 4,
|
|
573
|
+
ParameterStyle.NAMED_AT: 5,
|
|
574
|
+
ParameterStyle.NAMED_DOLLAR: 6,
|
|
575
|
+
ParameterStyle.NAMED_COLON: 7,
|
|
576
|
+
ParameterStyle.NAMED_PYFORMAT: 8,
|
|
577
|
+
}
|
|
362
578
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
579
|
+
return max(style_counts.keys(), key=lambda style: (style_counts[style], -precedence.get(style, 99)))
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
582
|
+
def find_filter(
|
|
583
|
+
filter_type: "type[FilterTypeT]",
|
|
584
|
+
filters: "Sequence[StatementFilter | StatementParameters] | Sequence[StatementFilter]",
|
|
585
|
+
) -> "FilterTypeT | None":
|
|
586
|
+
"""Get the filter specified by filter type from the filters.
|
|
366
587
|
|
|
367
588
|
Args:
|
|
368
|
-
|
|
589
|
+
filter_type: The type of filter to find.
|
|
590
|
+
filters: filter types to apply to the query
|
|
369
591
|
|
|
370
592
|
Returns:
|
|
371
|
-
The
|
|
593
|
+
The match filter instance or None
|
|
594
|
+
"""
|
|
595
|
+
return next(
|
|
596
|
+
(cast("FilterTypeT | None", filter_) for filter_ in filters if isinstance(filter_, filter_type)), None
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def _create_count_query(self, original_sql: "SQL") -> "SQL":
|
|
600
|
+
"""Create a COUNT query from the original SQL statement.
|
|
601
|
+
|
|
602
|
+
Transforms the original SELECT statement to count total rows while preserving
|
|
603
|
+
WHERE, HAVING, and GROUP BY clauses but removing ORDER BY, LIMIT, and OFFSET.
|
|
372
604
|
"""
|
|
373
|
-
|
|
605
|
+
if not original_sql.expression:
|
|
606
|
+
msg = "Cannot create COUNT query from empty SQL expression"
|
|
607
|
+
raise ImproperConfigurationError(msg)
|
|
608
|
+
expr = original_sql.expression
|
|
609
|
+
|
|
610
|
+
if isinstance(expr, exp.Select):
|
|
611
|
+
if expr.args.get("group"):
|
|
612
|
+
subquery = expr.subquery(alias="grouped_data")
|
|
613
|
+
count_expr = exp.select(exp.Count(this=exp.Star())).from_(subquery)
|
|
614
|
+
else:
|
|
615
|
+
count_expr = exp.select(exp.Count(this=exp.Star())).from_(
|
|
616
|
+
cast("exp.Expression", expr.args.get("from")), copy=False
|
|
617
|
+
)
|
|
618
|
+
if expr.args.get("where"):
|
|
619
|
+
count_expr = count_expr.where(cast("exp.Expression", expr.args.get("where")), copy=False)
|
|
620
|
+
if expr.args.get("having"):
|
|
621
|
+
count_expr = count_expr.having(cast("exp.Expression", expr.args.get("having")), copy=False)
|
|
622
|
+
|
|
623
|
+
count_expr.set("order", None)
|
|
624
|
+
count_expr.set("limit", None)
|
|
625
|
+
count_expr.set("offset", None)
|
|
626
|
+
|
|
627
|
+
return SQL(count_expr, *original_sql._positional_parameters, statement_config=original_sql.statement_config)
|
|
628
|
+
|
|
629
|
+
subquery = cast("exp.Select", expr).subquery(alias="total_query")
|
|
630
|
+
count_expr = exp.select(exp.Count(this=exp.Star())).from_(subquery)
|
|
631
|
+
return SQL(count_expr, *original_sql._positional_parameters, statement_config=original_sql.statement_config)
|