sqlspec 0.12.1__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/_sql.py +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +3 -3
- sqlspec/adapters/aiosqlite/driver.py +116 -141
- sqlspec/adapters/asyncmy/config.py +3 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +3 -7
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +231 -181
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +132 -124
- sqlspec/adapters/oracledb/config.py +6 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +3 -7
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +34 -30
- sqlspec/adapters/psycopg/driver.py +342 -214
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +150 -104
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +149 -216
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +31 -118
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +70 -23
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +102 -65
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +885 -379
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +82 -35
- sqlspec/storage/backends/obstore.py +66 -49
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
- sqlspec-0.13.0.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -170
- sqlspec-0.12.1.dist-info/RECORD +0 -145
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import TYPE_CHECKING,
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, cast
|
|
3
3
|
|
|
4
|
-
from sqlglot import exp
|
|
5
4
|
from sqlglot.optimizer import simplify
|
|
6
5
|
|
|
7
6
|
from sqlspec.exceptions import RiskLevel
|
|
8
|
-
from sqlspec.statement.pipelines
|
|
9
|
-
from sqlspec.statement.pipelines.result_types import TransformationLog, ValidationError
|
|
7
|
+
from sqlspec.statement.pipelines import ProcessorProtocol, TransformationLog, ValidationError
|
|
10
8
|
|
|
11
9
|
if TYPE_CHECKING:
|
|
12
|
-
from
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
|
|
12
|
+
from sqlspec.statement.pipelines import SQLProcessingContext
|
|
13
13
|
|
|
14
14
|
__all__ = ("ExpressionSimplifier", "SimplificationConfig")
|
|
15
15
|
|
|
@@ -26,20 +26,7 @@ class SimplificationConfig:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
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
|
-
"""
|
|
29
|
+
"""Advanced expression optimization using SQLGlot's simplification engine."""
|
|
43
30
|
|
|
44
31
|
def __init__(self, enabled: bool = True, config: Optional[SimplificationConfig] = None) -> None:
|
|
45
32
|
self.enabled = enabled
|
|
@@ -48,27 +35,20 @@ class ExpressionSimplifier(ProcessorProtocol):
|
|
|
48
35
|
def process(
|
|
49
36
|
self, expression: "Optional[exp.Expression]", context: "SQLProcessingContext"
|
|
50
37
|
) -> "Optional[exp.Expression]":
|
|
51
|
-
"""Process the expression to apply SQLGlot's simplification optimizations."""
|
|
52
38
|
if not self.enabled or expression is None:
|
|
53
39
|
return expression
|
|
54
40
|
|
|
55
41
|
original_sql = expression.sql(dialect=context.dialect)
|
|
56
42
|
|
|
57
|
-
# Extract placeholder info before simplification
|
|
58
|
-
placeholders_before = []
|
|
59
|
-
if context.merged_parameters:
|
|
60
|
-
placeholders_before = self._extract_placeholder_info(expression)
|
|
61
|
-
|
|
62
43
|
try:
|
|
63
44
|
simplified = simplify.simplify(
|
|
64
45
|
expression.copy(), constant_propagation=self.config.enable_literal_folding, dialect=context.dialect
|
|
65
46
|
)
|
|
66
47
|
except Exception as e:
|
|
67
|
-
# Add warning to context
|
|
68
48
|
error = ValidationError(
|
|
69
49
|
message=f"Expression simplification failed: {e}",
|
|
70
50
|
code="simplification-failed",
|
|
71
|
-
risk_level=RiskLevel.LOW,
|
|
51
|
+
risk_level=RiskLevel.LOW,
|
|
72
52
|
processor=self.__class__.__name__,
|
|
73
53
|
expression=expression,
|
|
74
54
|
)
|
|
@@ -78,7 +58,6 @@ class ExpressionSimplifier(ProcessorProtocol):
|
|
|
78
58
|
simplified_sql = simplified.sql(dialect=context.dialect)
|
|
79
59
|
chars_saved = len(original_sql) - len(simplified_sql)
|
|
80
60
|
|
|
81
|
-
# Log transformation
|
|
82
61
|
if original_sql != simplified_sql:
|
|
83
62
|
log = TransformationLog(
|
|
84
63
|
description=f"Simplified expression (saved {chars_saved} chars)",
|
|
@@ -88,169 +67,22 @@ class ExpressionSimplifier(ProcessorProtocol):
|
|
|
88
67
|
)
|
|
89
68
|
context.transformations.append(log)
|
|
90
69
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
70
|
+
optimizations = []
|
|
71
|
+
if self.config.enable_literal_folding:
|
|
72
|
+
optimizations.append("literal_folding")
|
|
73
|
+
if self.config.enable_boolean_optimization:
|
|
74
|
+
optimizations.append("boolean_optimization")
|
|
75
|
+
if self.config.enable_connector_optimization:
|
|
76
|
+
optimizations.append("connector_optimization")
|
|
77
|
+
if self.config.enable_equality_normalization:
|
|
78
|
+
optimizations.append("equality_normalization")
|
|
79
|
+
if self.config.enable_complement_removal:
|
|
80
|
+
optimizations.append("complement_removal")
|
|
94
81
|
|
|
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
82
|
context.metadata[self.__class__.__name__] = {
|
|
107
83
|
"simplified": original_sql != simplified_sql,
|
|
108
84
|
"chars_saved": chars_saved,
|
|
109
|
-
"optimizations_applied":
|
|
85
|
+
"optimizations_applied": optimizations,
|
|
110
86
|
}
|
|
111
87
|
|
|
112
88
|
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
|