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.

Files changed (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {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