sqlspec 0.11.1__py3-none-any.whl → 0.12.1__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 +16 -3
- sqlspec/_serialization.py +3 -10
- sqlspec/_sql.py +1147 -0
- sqlspec/_typing.py +343 -41
- sqlspec/adapters/adbc/__init__.py +2 -6
- sqlspec/adapters/adbc/config.py +474 -149
- sqlspec/adapters/adbc/driver.py +330 -621
- sqlspec/adapters/aiosqlite/__init__.py +2 -6
- sqlspec/adapters/aiosqlite/config.py +143 -57
- sqlspec/adapters/aiosqlite/driver.py +269 -431
- sqlspec/adapters/asyncmy/__init__.py +3 -8
- sqlspec/adapters/asyncmy/config.py +247 -202
- sqlspec/adapters/asyncmy/driver.py +218 -436
- sqlspec/adapters/asyncpg/__init__.py +4 -7
- sqlspec/adapters/asyncpg/config.py +329 -176
- sqlspec/adapters/asyncpg/driver.py +417 -487
- sqlspec/adapters/bigquery/__init__.py +2 -2
- sqlspec/adapters/bigquery/config.py +407 -0
- sqlspec/adapters/bigquery/driver.py +600 -553
- sqlspec/adapters/duckdb/__init__.py +4 -1
- sqlspec/adapters/duckdb/config.py +432 -321
- sqlspec/adapters/duckdb/driver.py +392 -406
- sqlspec/adapters/oracledb/__init__.py +3 -8
- sqlspec/adapters/oracledb/config.py +625 -0
- sqlspec/adapters/oracledb/driver.py +548 -921
- sqlspec/adapters/psqlpy/__init__.py +4 -7
- sqlspec/adapters/psqlpy/config.py +372 -203
- sqlspec/adapters/psqlpy/driver.py +197 -533
- sqlspec/adapters/psycopg/__init__.py +3 -8
- sqlspec/adapters/psycopg/config.py +725 -0
- sqlspec/adapters/psycopg/driver.py +734 -694
- sqlspec/adapters/sqlite/__init__.py +2 -6
- sqlspec/adapters/sqlite/config.py +146 -81
- sqlspec/adapters/sqlite/driver.py +242 -405
- sqlspec/base.py +220 -784
- sqlspec/config.py +354 -0
- sqlspec/driver/__init__.py +22 -0
- sqlspec/driver/_async.py +252 -0
- sqlspec/driver/_common.py +338 -0
- sqlspec/driver/_sync.py +261 -0
- sqlspec/driver/mixins/__init__.py +17 -0
- sqlspec/driver/mixins/_pipeline.py +523 -0
- sqlspec/driver/mixins/_result_utils.py +122 -0
- sqlspec/driver/mixins/_sql_translator.py +35 -0
- sqlspec/driver/mixins/_storage.py +993 -0
- sqlspec/driver/mixins/_type_coercion.py +131 -0
- sqlspec/exceptions.py +299 -7
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +474 -0
- sqlspec/extensions/litestar/__init__.py +1 -6
- sqlspec/extensions/litestar/_utils.py +1 -5
- sqlspec/extensions/litestar/config.py +5 -6
- sqlspec/extensions/litestar/handlers.py +13 -12
- sqlspec/extensions/litestar/plugin.py +22 -24
- sqlspec/extensions/litestar/providers.py +37 -55
- sqlspec/loader.py +528 -0
- sqlspec/service/__init__.py +3 -0
- sqlspec/service/base.py +24 -0
- sqlspec/service/pagination.py +26 -0
- sqlspec/statement/__init__.py +21 -0
- sqlspec/statement/builder/__init__.py +54 -0
- sqlspec/statement/builder/_ddl_utils.py +119 -0
- sqlspec/statement/builder/_parsing_utils.py +135 -0
- sqlspec/statement/builder/base.py +328 -0
- sqlspec/statement/builder/ddl.py +1379 -0
- sqlspec/statement/builder/delete.py +80 -0
- sqlspec/statement/builder/insert.py +274 -0
- sqlspec/statement/builder/merge.py +95 -0
- sqlspec/statement/builder/mixins/__init__.py +65 -0
- sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
- sqlspec/statement/builder/mixins/_case_builder.py +91 -0
- sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
- sqlspec/statement/builder/mixins/_delete_from.py +34 -0
- sqlspec/statement/builder/mixins/_from.py +61 -0
- sqlspec/statement/builder/mixins/_group_by.py +119 -0
- sqlspec/statement/builder/mixins/_having.py +35 -0
- sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
- sqlspec/statement/builder/mixins/_insert_into.py +36 -0
- sqlspec/statement/builder/mixins/_insert_values.py +69 -0
- sqlspec/statement/builder/mixins/_join.py +110 -0
- sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
- sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
- sqlspec/statement/builder/mixins/_order_by.py +46 -0
- sqlspec/statement/builder/mixins/_pivot.py +82 -0
- sqlspec/statement/builder/mixins/_returning.py +37 -0
- sqlspec/statement/builder/mixins/_select_columns.py +60 -0
- sqlspec/statement/builder/mixins/_set_ops.py +122 -0
- sqlspec/statement/builder/mixins/_unpivot.py +80 -0
- sqlspec/statement/builder/mixins/_update_from.py +54 -0
- sqlspec/statement/builder/mixins/_update_set.py +91 -0
- sqlspec/statement/builder/mixins/_update_table.py +29 -0
- sqlspec/statement/builder/mixins/_where.py +374 -0
- sqlspec/statement/builder/mixins/_window_functions.py +86 -0
- sqlspec/statement/builder/protocols.py +20 -0
- sqlspec/statement/builder/select.py +206 -0
- sqlspec/statement/builder/update.py +178 -0
- sqlspec/statement/filters.py +571 -0
- sqlspec/statement/parameters.py +736 -0
- sqlspec/statement/pipelines/__init__.py +67 -0
- sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
- sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
- sqlspec/statement/pipelines/base.py +315 -0
- sqlspec/statement/pipelines/context.py +119 -0
- sqlspec/statement/pipelines/result_types.py +41 -0
- sqlspec/statement/pipelines/transformers/__init__.py +8 -0
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
- sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
- sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
- sqlspec/statement/pipelines/validators/__init__.py +23 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
- sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
- sqlspec/statement/pipelines/validators/_performance.py +703 -0
- sqlspec/statement/pipelines/validators/_security.py +990 -0
- sqlspec/statement/pipelines/validators/base.py +67 -0
- sqlspec/statement/result.py +527 -0
- sqlspec/statement/splitter.py +701 -0
- sqlspec/statement/sql.py +1198 -0
- sqlspec/storage/__init__.py +15 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +166 -0
- sqlspec/storage/backends/fsspec.py +315 -0
- sqlspec/storage/backends/obstore.py +464 -0
- sqlspec/storage/protocol.py +170 -0
- sqlspec/storage/registry.py +315 -0
- sqlspec/typing.py +157 -36
- sqlspec/utils/correlation.py +155 -0
- sqlspec/utils/deprecation.py +3 -6
- sqlspec/utils/fixtures.py +6 -11
- sqlspec/utils/logging.py +135 -0
- sqlspec/utils/module_loader.py +45 -43
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +6 -8
- sqlspec/utils/sync_tools.py +15 -27
- sqlspec/utils/text.py +58 -26
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
- sqlspec-0.12.1.dist-info/RECORD +145 -0
- sqlspec/adapters/bigquery/config/__init__.py +0 -3
- sqlspec/adapters/bigquery/config/_common.py +0 -40
- sqlspec/adapters/bigquery/config/_sync.py +0 -87
- sqlspec/adapters/oracledb/config/__init__.py +0 -9
- sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
- sqlspec/adapters/oracledb/config/_common.py +0 -131
- sqlspec/adapters/oracledb/config/_sync.py +0 -186
- sqlspec/adapters/psycopg/config/__init__.py +0 -19
- sqlspec/adapters/psycopg/config/_async.py +0 -169
- sqlspec/adapters/psycopg/config/_common.py +0 -56
- sqlspec/adapters/psycopg/config/_sync.py +0 -168
- sqlspec/filters.py +0 -331
- sqlspec/mixins.py +0 -305
- sqlspec/statement.py +0 -378
- sqlspec-0.11.1.dist-info/RECORD +0 -69
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
3
|
+
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
from sqlglot.optimizer import simplify
|
|
6
|
+
|
|
7
|
+
from sqlspec.exceptions import RiskLevel
|
|
8
|
+
from sqlspec.statement.pipelines.base import ProcessorProtocol
|
|
9
|
+
from sqlspec.statement.pipelines.result_types import TransformationLog, ValidationError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sqlspec.statement.pipelines.context import SQLProcessingContext
|
|
13
|
+
|
|
14
|
+
__all__ = ("ExpressionSimplifier", "SimplificationConfig")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SimplificationConfig:
|
|
19
|
+
"""Configuration for expression simplification."""
|
|
20
|
+
|
|
21
|
+
enable_literal_folding: bool = True
|
|
22
|
+
enable_boolean_optimization: bool = True
|
|
23
|
+
enable_connector_optimization: bool = True
|
|
24
|
+
enable_equality_normalization: bool = True
|
|
25
|
+
enable_complement_removal: bool = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExpressionSimplifier(ProcessorProtocol):
|
|
29
|
+
"""Advanced expression optimization using SQLGlot's simplification engine.
|
|
30
|
+
|
|
31
|
+
This transformer applies SQLGlot's comprehensive simplification suite:
|
|
32
|
+
- Constant folding: 1 + 1 → 2
|
|
33
|
+
- Boolean logic optimization: (A AND B) OR (A AND C) → A AND (B OR C)
|
|
34
|
+
- Tautology removal: WHERE TRUE AND x = 1 → WHERE x = 1
|
|
35
|
+
- Dead code elimination: WHERE FALSE OR x = 1 → WHERE x = 1
|
|
36
|
+
- Double negative removal: NOT NOT x → x
|
|
37
|
+
- Expression standardization: Consistent operator precedence
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
enabled: Whether expression simplification is enabled.
|
|
41
|
+
config: Configuration object controlling which optimizations to apply.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, enabled: bool = True, config: Optional[SimplificationConfig] = None) -> None:
|
|
45
|
+
self.enabled = enabled
|
|
46
|
+
self.config = config or SimplificationConfig()
|
|
47
|
+
|
|
48
|
+
def process(
|
|
49
|
+
self, expression: "Optional[exp.Expression]", context: "SQLProcessingContext"
|
|
50
|
+
) -> "Optional[exp.Expression]":
|
|
51
|
+
"""Process the expression to apply SQLGlot's simplification optimizations."""
|
|
52
|
+
if not self.enabled or expression is None:
|
|
53
|
+
return expression
|
|
54
|
+
|
|
55
|
+
original_sql = expression.sql(dialect=context.dialect)
|
|
56
|
+
|
|
57
|
+
# Extract placeholder info before simplification
|
|
58
|
+
placeholders_before = []
|
|
59
|
+
if context.merged_parameters:
|
|
60
|
+
placeholders_before = self._extract_placeholder_info(expression)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
simplified = simplify.simplify(
|
|
64
|
+
expression.copy(), constant_propagation=self.config.enable_literal_folding, dialect=context.dialect
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# Add warning to context
|
|
68
|
+
error = ValidationError(
|
|
69
|
+
message=f"Expression simplification failed: {e}",
|
|
70
|
+
code="simplification-failed",
|
|
71
|
+
risk_level=RiskLevel.LOW, # Not critical
|
|
72
|
+
processor=self.__class__.__name__,
|
|
73
|
+
expression=expression,
|
|
74
|
+
)
|
|
75
|
+
context.validation_errors.append(error)
|
|
76
|
+
return expression
|
|
77
|
+
else:
|
|
78
|
+
simplified_sql = simplified.sql(dialect=context.dialect)
|
|
79
|
+
chars_saved = len(original_sql) - len(simplified_sql)
|
|
80
|
+
|
|
81
|
+
# Log transformation
|
|
82
|
+
if original_sql != simplified_sql:
|
|
83
|
+
log = TransformationLog(
|
|
84
|
+
description=f"Simplified expression (saved {chars_saved} chars)",
|
|
85
|
+
processor=self.__class__.__name__,
|
|
86
|
+
before=original_sql,
|
|
87
|
+
after=simplified_sql,
|
|
88
|
+
)
|
|
89
|
+
context.transformations.append(log)
|
|
90
|
+
|
|
91
|
+
# If we have parameters and SQL changed, check for parameter reordering
|
|
92
|
+
if context.merged_parameters and placeholders_before:
|
|
93
|
+
placeholders_after = self._extract_placeholder_info(simplified)
|
|
94
|
+
|
|
95
|
+
# Create parameter position mapping if placeholders were reordered
|
|
96
|
+
if len(placeholders_after) == len(placeholders_before):
|
|
97
|
+
parameter_mapping = self._create_parameter_mapping(placeholders_before, placeholders_after)
|
|
98
|
+
|
|
99
|
+
# Store mapping in context metadata for later use
|
|
100
|
+
if parameter_mapping and any(
|
|
101
|
+
new_pos != old_pos for new_pos, old_pos in parameter_mapping.items()
|
|
102
|
+
):
|
|
103
|
+
context.metadata["parameter_position_mapping"] = parameter_mapping
|
|
104
|
+
|
|
105
|
+
# Store metadata
|
|
106
|
+
context.metadata[self.__class__.__name__] = {
|
|
107
|
+
"simplified": original_sql != simplified_sql,
|
|
108
|
+
"chars_saved": chars_saved,
|
|
109
|
+
"optimizations_applied": self._get_applied_optimizations(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return cast("exp.Expression", simplified)
|
|
113
|
+
|
|
114
|
+
def _get_applied_optimizations(self) -> list[str]:
|
|
115
|
+
"""Get list of optimization types that are enabled."""
|
|
116
|
+
optimizations = []
|
|
117
|
+
if self.config.enable_literal_folding:
|
|
118
|
+
optimizations.append("literal_folding")
|
|
119
|
+
if self.config.enable_boolean_optimization:
|
|
120
|
+
optimizations.append("boolean_optimization")
|
|
121
|
+
if self.config.enable_connector_optimization:
|
|
122
|
+
optimizations.append("connector_optimization")
|
|
123
|
+
if self.config.enable_equality_normalization:
|
|
124
|
+
optimizations.append("equality_normalization")
|
|
125
|
+
if self.config.enable_complement_removal:
|
|
126
|
+
optimizations.append("complement_removal")
|
|
127
|
+
return optimizations
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _extract_placeholder_info(expression: "exp.Expression") -> list[dict[str, Any]]:
|
|
131
|
+
"""Extract information about placeholder positions in an expression.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of placeholder info dicts with position, comparison context, etc.
|
|
135
|
+
"""
|
|
136
|
+
placeholders = []
|
|
137
|
+
|
|
138
|
+
for node in expression.walk():
|
|
139
|
+
# Check for both Placeholder and Parameter nodes (sqlglot parses $1 as Parameter)
|
|
140
|
+
if isinstance(node, (exp.Placeholder, exp.Parameter)):
|
|
141
|
+
# Get comparison context for the placeholder
|
|
142
|
+
parent = node.parent
|
|
143
|
+
comparison_info = None
|
|
144
|
+
|
|
145
|
+
if isinstance(parent, (exp.GTE, exp.GT, exp.LTE, exp.LT, exp.EQ, exp.NEQ)):
|
|
146
|
+
# Get the column being compared
|
|
147
|
+
left = parent.this
|
|
148
|
+
right = parent.expression
|
|
149
|
+
|
|
150
|
+
# Determine which side the placeholder is on
|
|
151
|
+
if node == right:
|
|
152
|
+
side = "right"
|
|
153
|
+
column = left
|
|
154
|
+
else:
|
|
155
|
+
side = "left"
|
|
156
|
+
column = right
|
|
157
|
+
|
|
158
|
+
if isinstance(column, exp.Column):
|
|
159
|
+
comparison_info = {"column": column.name, "operator": parent.__class__.__name__, "side": side}
|
|
160
|
+
|
|
161
|
+
# Extract the placeholder index from its text
|
|
162
|
+
placeholder_text = str(node)
|
|
163
|
+
placeholder_index = None
|
|
164
|
+
|
|
165
|
+
# Handle different formats: "$1", "@1", ":1", etc.
|
|
166
|
+
if placeholder_text.startswith("$") and placeholder_text[1:].isdigit():
|
|
167
|
+
# PostgreSQL style: $1, $2, etc. (1-based)
|
|
168
|
+
placeholder_index = int(placeholder_text[1:]) - 1
|
|
169
|
+
elif placeholder_text.startswith("@") and placeholder_text[1:].isdigit():
|
|
170
|
+
# sqlglot internal representation: @1, @2, etc. (1-based)
|
|
171
|
+
placeholder_index = int(placeholder_text[1:]) - 1
|
|
172
|
+
elif placeholder_text.startswith(":") and placeholder_text[1:].isdigit():
|
|
173
|
+
# Oracle style: :1, :2, etc. (1-based)
|
|
174
|
+
placeholder_index = int(placeholder_text[1:]) - 1
|
|
175
|
+
|
|
176
|
+
placeholder_info = {
|
|
177
|
+
"node": node,
|
|
178
|
+
"parent": parent,
|
|
179
|
+
"comparison_info": comparison_info,
|
|
180
|
+
"index": placeholder_index,
|
|
181
|
+
}
|
|
182
|
+
placeholders.append(placeholder_info)
|
|
183
|
+
|
|
184
|
+
return placeholders
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _create_parameter_mapping(
|
|
188
|
+
placeholders_before: list[dict[str, Any]], placeholders_after: list[dict[str, Any]]
|
|
189
|
+
) -> dict[int, int]:
|
|
190
|
+
"""Create a mapping of parameter positions from transformed SQL back to original positions.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
placeholders_before: Placeholder info from original expression
|
|
194
|
+
placeholders_after: Placeholder info from transformed expression
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dict mapping new positions to original positions
|
|
198
|
+
"""
|
|
199
|
+
mapping = {}
|
|
200
|
+
|
|
201
|
+
# For simplicity, if we have placeholder indices, use them directly
|
|
202
|
+
# This handles numeric placeholders like $1, $2
|
|
203
|
+
if all(ph.get("index") is not None for ph in placeholders_before + placeholders_after):
|
|
204
|
+
for new_pos, ph_after in enumerate(placeholders_after):
|
|
205
|
+
# The placeholder index tells us which original parameter this refers to
|
|
206
|
+
original_index = ph_after["index"]
|
|
207
|
+
if original_index is not None:
|
|
208
|
+
mapping[new_pos] = original_index
|
|
209
|
+
return mapping
|
|
210
|
+
|
|
211
|
+
# For more complex cases, we need to match based on comparison context
|
|
212
|
+
# Map placeholders based on their comparison context and column
|
|
213
|
+
for new_pos, ph_after in enumerate(placeholders_after):
|
|
214
|
+
after_info = ph_after["comparison_info"]
|
|
215
|
+
|
|
216
|
+
if after_info:
|
|
217
|
+
# For flipped comparisons (e.g., "value >= $1" becomes "$1 <= value")
|
|
218
|
+
# we need to match based on the semantic meaning, not just the operator
|
|
219
|
+
|
|
220
|
+
# First, try to find exact match based on column and operator meaning
|
|
221
|
+
for old_pos, ph_before in enumerate(placeholders_before):
|
|
222
|
+
before_info = ph_before["comparison_info"]
|
|
223
|
+
|
|
224
|
+
if before_info and before_info["column"] == after_info["column"]:
|
|
225
|
+
# Check if this is a flipped comparison
|
|
226
|
+
# "value >= X" is semantically equivalent to "X <= value"
|
|
227
|
+
# "value <= X" is semantically equivalent to "X >= value"
|
|
228
|
+
|
|
229
|
+
before_op = before_info["operator"]
|
|
230
|
+
after_op = after_info["operator"]
|
|
231
|
+
before_side = before_info["side"]
|
|
232
|
+
after_side = after_info["side"]
|
|
233
|
+
|
|
234
|
+
# If sides are different, operators might be flipped
|
|
235
|
+
if before_side != after_side:
|
|
236
|
+
# Map flipped operators
|
|
237
|
+
op_flip_map = {
|
|
238
|
+
("GTE", "right", "LTE", "left"): True, # value >= X -> X <= value
|
|
239
|
+
("LTE", "right", "GTE", "left"): True, # value <= X -> X >= value
|
|
240
|
+
("GT", "right", "LT", "left"): True, # value > X -> X < value
|
|
241
|
+
("LT", "right", "GT", "left"): True, # value < X -> X > value
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if op_flip_map.get((before_op, before_side, after_op, after_side)):
|
|
245
|
+
mapping[new_pos] = old_pos
|
|
246
|
+
break
|
|
247
|
+
# Same side, same operator - direct match
|
|
248
|
+
elif before_op == after_op:
|
|
249
|
+
mapping[new_pos] = old_pos
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
# If no comparison context or no match found, try to map by position
|
|
253
|
+
if new_pos not in mapping and new_pos < len(placeholders_before):
|
|
254
|
+
mapping[new_pos] = new_pos
|
|
255
|
+
|
|
256
|
+
return mapping
|