sqlspec 0.12.2__py3-none-any.whl → 0.13.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.

Files changed (113) hide show
  1. sqlspec/_sql.py +21 -180
  2. sqlspec/adapters/adbc/config.py +10 -12
  3. sqlspec/adapters/adbc/driver.py +120 -118
  4. sqlspec/adapters/aiosqlite/config.py +16 -3
  5. sqlspec/adapters/aiosqlite/driver.py +100 -130
  6. sqlspec/adapters/asyncmy/config.py +17 -4
  7. sqlspec/adapters/asyncmy/driver.py +123 -135
  8. sqlspec/adapters/asyncpg/config.py +17 -29
  9. sqlspec/adapters/asyncpg/driver.py +98 -140
  10. sqlspec/adapters/bigquery/config.py +4 -5
  11. sqlspec/adapters/bigquery/driver.py +125 -167
  12. sqlspec/adapters/duckdb/config.py +3 -6
  13. sqlspec/adapters/duckdb/driver.py +114 -111
  14. sqlspec/adapters/oracledb/config.py +32 -5
  15. sqlspec/adapters/oracledb/driver.py +242 -259
  16. sqlspec/adapters/psqlpy/config.py +18 -9
  17. sqlspec/adapters/psqlpy/driver.py +118 -93
  18. sqlspec/adapters/psycopg/config.py +44 -31
  19. sqlspec/adapters/psycopg/driver.py +283 -236
  20. sqlspec/adapters/sqlite/config.py +3 -3
  21. sqlspec/adapters/sqlite/driver.py +103 -97
  22. sqlspec/config.py +0 -4
  23. sqlspec/driver/_async.py +89 -98
  24. sqlspec/driver/_common.py +52 -17
  25. sqlspec/driver/_sync.py +81 -105
  26. sqlspec/driver/connection.py +207 -0
  27. sqlspec/driver/mixins/_csv_writer.py +91 -0
  28. sqlspec/driver/mixins/_pipeline.py +38 -49
  29. sqlspec/driver/mixins/_result_utils.py +27 -9
  30. sqlspec/driver/mixins/_storage.py +67 -181
  31. sqlspec/driver/mixins/_type_coercion.py +3 -4
  32. sqlspec/driver/parameters.py +138 -0
  33. sqlspec/exceptions.py +10 -2
  34. sqlspec/extensions/aiosql/adapter.py +0 -10
  35. sqlspec/extensions/litestar/handlers.py +0 -1
  36. sqlspec/extensions/litestar/plugin.py +0 -3
  37. sqlspec/extensions/litestar/providers.py +0 -14
  38. sqlspec/loader.py +25 -90
  39. sqlspec/protocols.py +542 -0
  40. sqlspec/service/__init__.py +3 -2
  41. sqlspec/service/_util.py +147 -0
  42. sqlspec/service/base.py +1116 -9
  43. sqlspec/statement/builder/__init__.py +42 -32
  44. sqlspec/statement/builder/_ddl_utils.py +0 -10
  45. sqlspec/statement/builder/_parsing_utils.py +10 -4
  46. sqlspec/statement/builder/base.py +67 -22
  47. sqlspec/statement/builder/column.py +283 -0
  48. sqlspec/statement/builder/ddl.py +91 -67
  49. sqlspec/statement/builder/delete.py +23 -7
  50. sqlspec/statement/builder/insert.py +29 -15
  51. sqlspec/statement/builder/merge.py +4 -4
  52. sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
  53. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
  54. sqlspec/statement/builder/mixins/_delete_from.py +1 -1
  55. sqlspec/statement/builder/mixins/_from.py +10 -8
  56. sqlspec/statement/builder/mixins/_group_by.py +0 -1
  57. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
  58. sqlspec/statement/builder/mixins/_insert_values.py +0 -2
  59. sqlspec/statement/builder/mixins/_join.py +20 -13
  60. sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
  61. sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
  62. sqlspec/statement/builder/mixins/_order_by.py +2 -2
  63. sqlspec/statement/builder/mixins/_pivot.py +4 -7
  64. sqlspec/statement/builder/mixins/_select_columns.py +6 -5
  65. sqlspec/statement/builder/mixins/_unpivot.py +6 -9
  66. sqlspec/statement/builder/mixins/_update_from.py +2 -1
  67. sqlspec/statement/builder/mixins/_update_set.py +11 -8
  68. sqlspec/statement/builder/mixins/_where.py +61 -34
  69. sqlspec/statement/builder/select.py +32 -17
  70. sqlspec/statement/builder/update.py +25 -11
  71. sqlspec/statement/filters.py +39 -14
  72. sqlspec/statement/parameter_manager.py +220 -0
  73. sqlspec/statement/parameters.py +210 -79
  74. sqlspec/statement/pipelines/__init__.py +166 -23
  75. sqlspec/statement/pipelines/analyzers/_analyzer.py +21 -20
  76. sqlspec/statement/pipelines/context.py +35 -39
  77. sqlspec/statement/pipelines/transformers/__init__.py +2 -3
  78. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
  79. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +628 -58
  80. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
  81. sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
  82. sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
  83. sqlspec/statement/pipelines/validators/_performance.py +38 -23
  84. sqlspec/statement/pipelines/validators/_security.py +39 -62
  85. sqlspec/statement/result.py +37 -129
  86. sqlspec/statement/splitter.py +0 -12
  87. sqlspec/statement/sql.py +863 -391
  88. sqlspec/statement/sql_compiler.py +140 -0
  89. sqlspec/storage/__init__.py +10 -2
  90. sqlspec/storage/backends/fsspec.py +53 -8
  91. sqlspec/storage/backends/obstore.py +15 -19
  92. sqlspec/storage/capabilities.py +101 -0
  93. sqlspec/storage/registry.py +56 -83
  94. sqlspec/typing.py +6 -434
  95. sqlspec/utils/cached_property.py +25 -0
  96. sqlspec/utils/correlation.py +0 -2
  97. sqlspec/utils/logging.py +0 -6
  98. sqlspec/utils/sync_tools.py +0 -4
  99. sqlspec/utils/text.py +0 -5
  100. sqlspec/utils/type_guards.py +892 -0
  101. {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/METADATA +1 -1
  102. sqlspec-0.13.1.dist-info/RECORD +150 -0
  103. sqlspec/statement/builder/protocols.py +0 -20
  104. sqlspec/statement/pipelines/base.py +0 -315
  105. sqlspec/statement/pipelines/result_types.py +0 -41
  106. sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
  107. sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
  108. sqlspec/statement/pipelines/validators/base.py +0 -67
  109. sqlspec/storage/protocol.py +0 -173
  110. sqlspec-0.12.2.dist-info/RECORD +0 -145
  111. {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/WHEEL +0 -0
  112. {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/LICENSE +0 -0
  113. {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,15 +1,15 @@
1
1
  from dataclasses import dataclass
2
- from typing import TYPE_CHECKING, Any, Optional, cast
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.base import ProcessorProtocol
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 sqlspec.statement.pipelines.context import SQLProcessingContext
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, # Not critical
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
- # 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)
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": self._get_applied_optimizations(),
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