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.

Files changed (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,222 @@
1
+ """CTE (Common Table Expression) and Set Operations mixins for SQL builders."""
2
+
3
+ from typing import Any, Optional, Union
4
+
5
+ from mypy_extensions import trait
6
+ from sqlglot import exp
7
+ from typing_extensions import Self
8
+
9
+ from sqlspec.exceptions import SQLBuilderError
10
+
11
+ __all__ = ("CommonTableExpressionMixin", "SetOperationMixin")
12
+
13
+
14
+ @trait
15
+ class CommonTableExpressionMixin:
16
+ """Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
17
+
18
+ __slots__ = ()
19
+ # Type annotation for PyRight - this will be provided by the base class
20
+ _expression: Optional[exp.Expression]
21
+
22
+ _with_ctes: Any # Provided by QueryBuilder
23
+ dialect: Any # Provided by QueryBuilder
24
+
25
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
26
+ """Add parameter - provided by QueryBuilder."""
27
+ msg = "Method must be provided by QueryBuilder subclass"
28
+ raise NotImplementedError(msg)
29
+
30
+ def with_(
31
+ self, name: str, query: Union[Any, str], recursive: bool = False, columns: Optional[list[str]] = None
32
+ ) -> Self:
33
+ """Add WITH clause (Common Table Expression).
34
+
35
+ Args:
36
+ name: The name of the CTE.
37
+ query: The query for the CTE (builder instance or SQL string).
38
+ recursive: Whether this is a recursive CTE.
39
+ columns: Optional column names for the CTE.
40
+
41
+ Raises:
42
+ SQLBuilderError: If the query type is unsupported.
43
+
44
+ Returns:
45
+ The current builder instance for method chaining.
46
+ """
47
+ if self._expression is None:
48
+ msg = "Cannot add WITH clause: expression not initialized."
49
+ raise SQLBuilderError(msg)
50
+
51
+ if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
52
+ msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
53
+ raise SQLBuilderError(msg)
54
+
55
+ cte_expr: Optional[exp.Expression] = None
56
+ if isinstance(query, str):
57
+ cte_expr = exp.maybe_parse(query, dialect=self.dialect)
58
+ elif isinstance(query, exp.Expression):
59
+ cte_expr = query
60
+ else:
61
+ built_query = query.to_statement()
62
+ cte_sql = built_query.sql
63
+ cte_expr = exp.maybe_parse(cte_sql, dialect=self.dialect)
64
+
65
+ parameters = built_query.parameters
66
+ if parameters:
67
+ if isinstance(parameters, dict):
68
+ for param_name, param_value in parameters.items():
69
+ self.add_parameter(param_value, name=param_name)
70
+ elif isinstance(parameters, (list, tuple)):
71
+ for param_value in parameters:
72
+ self.add_parameter(param_value)
73
+
74
+ if not cte_expr:
75
+ msg = f"Could not parse CTE query: {query}"
76
+ raise SQLBuilderError(msg)
77
+
78
+ if columns:
79
+ cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
80
+ else:
81
+ cte_alias_expr = exp.alias_(cte_expr, name)
82
+
83
+ existing_with = self._expression.args.get("with")
84
+ if existing_with:
85
+ existing_with.expressions.append(cte_alias_expr)
86
+ if recursive:
87
+ existing_with.set("recursive", recursive)
88
+ else:
89
+ # Only SELECT, INSERT, UPDATE support WITH clauses
90
+ if hasattr(self._expression, "with_") and isinstance(
91
+ self._expression, (exp.Select, exp.Insert, exp.Update)
92
+ ):
93
+ self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False)
94
+ if recursive:
95
+ with_clause = self._expression.find(exp.With)
96
+ if with_clause:
97
+ with_clause.set("recursive", recursive)
98
+ self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name))
99
+
100
+ return self
101
+
102
+
103
+ @trait
104
+ class SetOperationMixin:
105
+ """Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
106
+
107
+ __slots__ = ()
108
+ # Type annotation for PyRight - this will be provided by the base class
109
+ _expression: Optional[exp.Expression]
110
+
111
+ _parameters: dict[str, Any]
112
+ dialect: Any = None
113
+
114
+ def build(self) -> Any:
115
+ """Build the query - provided by QueryBuilder."""
116
+ msg = "Method must be provided by QueryBuilder subclass"
117
+ raise NotImplementedError(msg)
118
+
119
+ def union(self, other: Any, all_: bool = False) -> Self:
120
+ """Combine this query with another using UNION.
121
+
122
+ Args:
123
+ other: Another SelectBuilder or compatible builder to union with.
124
+ all_: If True, use UNION ALL instead of UNION.
125
+
126
+ Raises:
127
+ SQLBuilderError: If the current expression is not a SELECT statement.
128
+
129
+ Returns:
130
+ The new builder instance for the union query.
131
+ """
132
+ left_query = self.build()
133
+ right_query = other.build()
134
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
135
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
136
+ if not left_expr or not right_expr:
137
+ msg = "Could not parse queries for UNION operation"
138
+ raise SQLBuilderError(msg)
139
+ union_expr = exp.union(left_expr, right_expr, distinct=not all_)
140
+ new_builder = type(self)()
141
+ new_builder.dialect = self.dialect
142
+ new_builder._expression = union_expr
143
+ merged_parameters = dict(left_query.parameters)
144
+ for param_name, param_value in right_query.parameters.items():
145
+ if param_name in merged_parameters:
146
+ counter = 1
147
+ new_param_name = f"{param_name}_right_{counter}"
148
+ while new_param_name in merged_parameters:
149
+ counter += 1
150
+ new_param_name = f"{param_name}_right_{counter}"
151
+
152
+ def rename_parameter(
153
+ node: exp.Expression, old_name: str = param_name, new_name: str = new_param_name
154
+ ) -> exp.Expression:
155
+ if isinstance(node, exp.Placeholder) and node.name == old_name:
156
+ return exp.Placeholder(this=new_name)
157
+ return node
158
+
159
+ right_expr = right_expr.transform(rename_parameter)
160
+ union_expr = exp.union(left_expr, right_expr, distinct=not all_)
161
+ new_builder._expression = union_expr
162
+ merged_parameters[new_param_name] = param_value
163
+ else:
164
+ merged_parameters[param_name] = param_value
165
+ new_builder._parameters = merged_parameters
166
+ return new_builder
167
+
168
+ def intersect(self, other: Any) -> Self:
169
+ """Add INTERSECT clause.
170
+
171
+ Args:
172
+ other: Another SelectBuilder or compatible builder to intersect with.
173
+
174
+ Raises:
175
+ SQLBuilderError: If the current expression is not a SELECT statement.
176
+
177
+ Returns:
178
+ The new builder instance for the intersect query.
179
+ """
180
+ left_query = self.build()
181
+ right_query = other.build()
182
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
183
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
184
+ if not left_expr or not right_expr:
185
+ msg = "Could not parse queries for INTERSECT operation"
186
+ raise SQLBuilderError(msg)
187
+ intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
188
+ new_builder = type(self)()
189
+ new_builder.dialect = self.dialect
190
+ new_builder._expression = intersect_expr
191
+ merged_parameters = dict(left_query.parameters)
192
+ merged_parameters.update(right_query.parameters)
193
+ new_builder._parameters = merged_parameters
194
+ return new_builder
195
+
196
+ def except_(self, other: Any) -> Self:
197
+ """Combine this query with another using EXCEPT.
198
+
199
+ Args:
200
+ other: Another SelectBuilder or compatible builder to except with.
201
+
202
+ Raises:
203
+ SQLBuilderError: If the current expression is not a SELECT statement.
204
+
205
+ Returns:
206
+ The new builder instance for the except query.
207
+ """
208
+ left_query = self.build()
209
+ right_query = other.build()
210
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
211
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
212
+ if not left_expr or not right_expr:
213
+ msg = "Could not parse queries for EXCEPT operation"
214
+ raise SQLBuilderError(msg)
215
+ except_expr = exp.except_(left_expr, right_expr)
216
+ new_builder = type(self)()
217
+ new_builder.dialect = self.dialect
218
+ new_builder._expression = except_expr
219
+ merged_parameters = dict(left_query.parameters)
220
+ merged_parameters.update(right_query.parameters)
221
+ new_builder._parameters = merged_parameters
222
+ return new_builder
@@ -1,5 +1,8 @@
1
+ """Delete operation mixins for SQL builders."""
2
+
1
3
  from typing import Optional
2
4
 
5
+ from mypy_extensions import trait
3
6
  from sqlglot import exp
4
7
  from typing_extensions import Self
5
8
 
@@ -8,10 +11,14 @@ from sqlspec.exceptions import SQLBuilderError
8
11
  __all__ = ("DeleteFromClauseMixin",)
9
12
 
10
13
 
14
+ @trait
11
15
  class DeleteFromClauseMixin:
12
16
  """Mixin providing FROM clause for DELETE builders."""
13
17
 
14
- _expression: Optional[exp.Expression] = None
18
+ __slots__ = ()
19
+
20
+ # Type annotation for PyRight - this will be provided by the base class
21
+ _expression: Optional[exp.Expression]
15
22
 
16
23
  def from_(self, table: str) -> Self:
17
24
  """Set the target table for the DELETE statement.
@@ -0,0 +1,244 @@
1
+ """Insert operation mixins for SQL builders."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any, Optional, TypeVar, Union
5
+
6
+ from mypy_extensions import trait
7
+ from sqlglot import exp
8
+ from typing_extensions import Self
9
+
10
+ from sqlspec.exceptions import SQLBuilderError
11
+ from sqlspec.protocols import SQLBuilderProtocol
12
+
13
+ BuilderT = TypeVar("BuilderT", bound=SQLBuilderProtocol)
14
+
15
+ __all__ = ("InsertFromSelectMixin", "InsertIntoClauseMixin", "InsertValuesMixin")
16
+
17
+
18
+ @trait
19
+ class InsertIntoClauseMixin:
20
+ """Mixin providing INTO clause for INSERT builders."""
21
+
22
+ __slots__ = ()
23
+
24
+ # Type annotation for PyRight - this will be provided by the base class
25
+ _expression: Optional[exp.Expression]
26
+
27
+ def into(self, table: str) -> Self:
28
+ """Set the target table for the INSERT statement.
29
+
30
+ Args:
31
+ table: The name of the table to insert data into.
32
+
33
+ Raises:
34
+ SQLBuilderError: If the current expression is not an INSERT statement.
35
+
36
+ Returns:
37
+ The current builder instance for method chaining.
38
+ """
39
+ if self._expression is None:
40
+ self._expression = exp.Insert()
41
+ if not isinstance(self._expression, exp.Insert):
42
+ msg = "Cannot set target table on a non-INSERT expression."
43
+ raise SQLBuilderError(msg)
44
+
45
+ setattr(self, "_table", table)
46
+ self._expression.set("this", exp.to_table(table))
47
+ return self
48
+
49
+
50
+ @trait
51
+ class InsertValuesMixin:
52
+ """Mixin providing VALUES and columns methods for INSERT builders."""
53
+
54
+ __slots__ = ()
55
+
56
+ # Type annotation for PyRight - this will be provided by the base class
57
+ _expression: Optional[exp.Expression]
58
+
59
+ _columns: Any # Provided by QueryBuilder
60
+
61
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
62
+ """Add parameter - provided by QueryBuilder."""
63
+ msg = "Method must be provided by QueryBuilder subclass"
64
+ raise NotImplementedError(msg)
65
+
66
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
67
+ """Generate unique parameter name - provided by QueryBuilder."""
68
+ msg = "Method must be provided by QueryBuilder subclass"
69
+ raise NotImplementedError(msg)
70
+
71
+ def columns(self, *columns: Union[str, exp.Expression]) -> Self:
72
+ """Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
73
+ if self._expression is None:
74
+ self._expression = exp.Insert()
75
+ if not isinstance(self._expression, exp.Insert):
76
+ msg = "Cannot set columns on a non-INSERT expression."
77
+ raise SQLBuilderError(msg)
78
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
79
+ self._expression.set("columns", column_exprs)
80
+ try:
81
+ cols = self._columns
82
+ if not columns:
83
+ cols.clear()
84
+ else:
85
+ cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
86
+ except AttributeError:
87
+ pass
88
+ return self
89
+
90
+ def values(self, *values: Any, **kwargs: Any) -> Self:
91
+ """Add a row of values to the INSERT statement.
92
+
93
+ Supports:
94
+ - values(val1, val2, val3)
95
+ - values(col1=val1, col2=val2)
96
+ - values(mapping)
97
+
98
+ Args:
99
+ *values: Either positional values or a single mapping.
100
+ **kwargs: Column-value pairs.
101
+
102
+ Returns:
103
+ The current builder instance for method chaining.
104
+ """
105
+ if self._expression is None:
106
+ self._expression = exp.Insert()
107
+ if not isinstance(self._expression, exp.Insert):
108
+ msg = "Cannot add values to a non-INSERT expression."
109
+ raise SQLBuilderError(msg)
110
+
111
+ if kwargs:
112
+ if values:
113
+ msg = "Cannot mix positional values with keyword values."
114
+ raise SQLBuilderError(msg)
115
+ try:
116
+ _columns = self._columns
117
+ if not _columns:
118
+ self.columns(*kwargs.keys())
119
+ except AttributeError:
120
+ pass
121
+ row_exprs = []
122
+ for col, val in kwargs.items():
123
+ if isinstance(val, exp.Expression):
124
+ row_exprs.append(val)
125
+ else:
126
+ column_name = col if isinstance(col, str) else str(col)
127
+ if "." in column_name:
128
+ column_name = column_name.split(".")[-1]
129
+ param_name = self._generate_unique_parameter_name(column_name)
130
+ _, param_name = self.add_parameter(val, name=param_name)
131
+ row_exprs.append(exp.var(param_name))
132
+ elif len(values) == 1 and hasattr(values[0], "items"):
133
+ mapping = values[0]
134
+ try:
135
+ _columns = self._columns
136
+ if not _columns:
137
+ self.columns(*mapping.keys())
138
+ except AttributeError:
139
+ pass
140
+ row_exprs = []
141
+ for col, val in mapping.items():
142
+ if isinstance(val, exp.Expression):
143
+ row_exprs.append(val)
144
+ else:
145
+ column_name = col if isinstance(col, str) else str(col)
146
+ if "." in column_name:
147
+ column_name = column_name.split(".")[-1]
148
+ param_name = self._generate_unique_parameter_name(column_name)
149
+ _, param_name = self.add_parameter(val, name=param_name)
150
+ row_exprs.append(exp.var(param_name))
151
+ else:
152
+ try:
153
+ _columns = self._columns
154
+ if _columns and len(values) != len(_columns):
155
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
156
+ raise SQLBuilderError(msg)
157
+ except AttributeError:
158
+ pass
159
+ row_exprs = []
160
+ for i, v in enumerate(values):
161
+ if isinstance(v, exp.Expression):
162
+ row_exprs.append(v)
163
+ else:
164
+ try:
165
+ _columns = self._columns
166
+ if _columns and i < len(_columns):
167
+ column_name = (
168
+ str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
169
+ )
170
+ param_name = self._generate_unique_parameter_name(column_name)
171
+ else:
172
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
173
+ except AttributeError:
174
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
175
+ _, param_name = self.add_parameter(v, name=param_name)
176
+ row_exprs.append(exp.var(param_name))
177
+
178
+ values_expr = exp.Values(expressions=[row_exprs])
179
+ self._expression.set("expression", values_expr)
180
+ return self
181
+
182
+ def add_values(self, values: Sequence[Any]) -> Self:
183
+ """Add a row of values to the INSERT statement (alternative signature).
184
+
185
+ Args:
186
+ values: Sequence of values for the row.
187
+
188
+ Returns:
189
+ The current builder instance for method chaining.
190
+ """
191
+ return self.values(*values)
192
+
193
+
194
+ @trait
195
+ class InsertFromSelectMixin:
196
+ """Mixin providing INSERT ... SELECT support for INSERT builders."""
197
+
198
+ __slots__ = ()
199
+
200
+ # Type annotation for PyRight - this will be provided by the base class
201
+ _expression: Optional[exp.Expression]
202
+
203
+ _table: Any # Provided by QueryBuilder
204
+
205
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
206
+ """Add parameter - provided by QueryBuilder."""
207
+ msg = "Method must be provided by QueryBuilder subclass"
208
+ raise NotImplementedError(msg)
209
+
210
+ def from_select(self, select_builder: Any) -> Self:
211
+ """Sets the INSERT source to a SELECT statement.
212
+
213
+ Args:
214
+ select_builder: A SelectBuilder instance representing the SELECT query.
215
+
216
+ Returns:
217
+ The current builder instance for method chaining.
218
+
219
+ Raises:
220
+ SQLBuilderError: If the table is not set or the select_builder is invalid.
221
+ """
222
+ try:
223
+ if not self._table:
224
+ msg = "The target table must be set using .into() before adding values."
225
+ raise SQLBuilderError(msg)
226
+ except AttributeError:
227
+ msg = "The target table must be set using .into() before adding values."
228
+ raise SQLBuilderError(msg)
229
+ if self._expression is None:
230
+ self._expression = exp.Insert()
231
+ if not isinstance(self._expression, exp.Insert):
232
+ msg = "Cannot set INSERT source on a non-INSERT expression."
233
+ raise SQLBuilderError(msg)
234
+ subquery_parameters = select_builder._parameters
235
+ if subquery_parameters:
236
+ for p_name, p_value in subquery_parameters.items():
237
+ self.add_parameter(p_value, name=p_name)
238
+ select_expr = select_builder._expression
239
+ if select_expr and isinstance(select_expr, exp.Select):
240
+ self._expression.set("expression", select_expr.copy())
241
+ else:
242
+ msg = "SelectBuilder must have a valid SELECT expression."
243
+ raise SQLBuilderError(msg)
244
+ return self
@@ -1,25 +1,33 @@
1
1
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
2
2
 
3
+ from mypy_extensions import trait
3
4
  from sqlglot import exp
4
5
  from typing_extensions import Self
5
6
 
7
+ from sqlspec.builder._parsing_utils import parse_table_expression
6
8
  from sqlspec.exceptions import SQLBuilderError
7
- from sqlspec.statement.builder._parsing_utils import parse_table_expression
8
9
  from sqlspec.utils.type_guards import has_query_builder_parameters
9
10
 
10
11
  if TYPE_CHECKING:
12
+ from sqlspec.core.statement import SQL
11
13
  from sqlspec.protocols import SQLBuilderProtocol
12
14
 
13
15
  __all__ = ("JoinClauseMixin",)
14
16
 
15
17
 
18
+ @trait
16
19
  class JoinClauseMixin:
17
20
  """Mixin providing JOIN clause methods for SELECT builders."""
18
21
 
22
+ __slots__ = ()
23
+
24
+ # Type annotation for PyRight - this will be provided by the base class
25
+ _expression: Optional[exp.Expression]
26
+
19
27
  def join(
20
28
  self,
21
29
  table: Union[str, exp.Expression, Any],
22
- on: Optional[Union[str, exp.Expression]] = None,
30
+ on: Optional[Union[str, exp.Expression, "SQL"]] = None,
23
31
  alias: Optional[str] = None,
24
32
  join_type: str = "INNER",
25
33
  ) -> Self:
@@ -33,25 +41,49 @@ class JoinClauseMixin:
33
41
  if isinstance(table, str):
34
42
  table_expr = parse_table_expression(table, alias)
35
43
  elif has_query_builder_parameters(table):
36
- # Work directly with AST when possible to avoid string parsing
37
44
  if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
38
45
  table_expr_value = getattr(table, "_expression", None)
39
46
  if table_expr_value is not None:
40
- subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
47
+ subquery_exp = exp.paren(table_expr_value)
41
48
  else:
42
49
  subquery_exp = exp.paren(exp.Anonymous(this=""))
43
50
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
44
51
  else:
45
- subquery = table.build() # pyright: ignore
52
+ subquery = table.build()
46
53
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
47
54
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
48
55
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
49
- # Parameter merging logic can be added here if needed
50
56
  else:
51
57
  table_expr = table
52
58
  on_expr: Optional[exp.Expression] = None
53
59
  if on is not None:
54
- on_expr = exp.condition(on) if isinstance(on, str) else on
60
+ if isinstance(on, str):
61
+ on_expr = exp.condition(on)
62
+ elif hasattr(on, "expression") and hasattr(on, "sql"):
63
+ # Handle SQL objects (from sql.raw with parameters)
64
+ expression = getattr(on, "expression", None)
65
+ if expression is not None and isinstance(expression, exp.Expression):
66
+ # Merge parameters from SQL object into builder
67
+ if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
68
+ sql_parameters = getattr(on, "parameters", {})
69
+ for param_name, param_value in sql_parameters.items():
70
+ builder.add_parameter(param_value, name=param_name)
71
+ on_expr = expression
72
+ else:
73
+ # If expression is None, fall back to parsing the raw SQL
74
+ sql_text = getattr(on, "sql", "")
75
+ # Merge parameters even when parsing raw SQL
76
+ if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
77
+ sql_parameters = getattr(on, "parameters", {})
78
+ for param_name, param_value in sql_parameters.items():
79
+ builder.add_parameter(param_value, name=param_name)
80
+ on_expr = exp.maybe_parse(sql_text) or exp.condition(str(sql_text))
81
+ # For other types (should be exp.Expression)
82
+ elif isinstance(on, exp.Expression):
83
+ on_expr = on
84
+ else:
85
+ # Last resort - convert to string and parse
86
+ on_expr = exp.condition(str(on))
55
87
  join_type_upper = join_type.upper()
56
88
  if join_type_upper == "INNER":
57
89
  join_expr = exp.Join(this=table_expr, on=on_expr)
@@ -68,22 +100,22 @@ class JoinClauseMixin:
68
100
  return cast("Self", builder)
69
101
 
70
102
  def inner_join(
71
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
103
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
72
104
  ) -> Self:
73
105
  return self.join(table, on, alias, "INNER")
74
106
 
75
107
  def left_join(
76
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
108
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
77
109
  ) -> Self:
78
110
  return self.join(table, on, alias, "LEFT")
79
111
 
80
112
  def right_join(
81
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
113
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
82
114
  ) -> Self:
83
115
  return self.join(table, on, alias, "RIGHT")
84
116
 
85
117
  def full_join(
86
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
118
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
87
119
  ) -> Self:
88
120
  return self.join(table, on, alias, "FULL")
89
121
 
@@ -101,12 +133,12 @@ class JoinClauseMixin:
101
133
  if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
102
134
  table_expr_value = getattr(table, "_expression", None)
103
135
  if table_expr_value is not None:
104
- subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
136
+ subquery_exp = exp.paren(table_expr_value)
105
137
  else:
106
138
  subquery_exp = exp.paren(exp.Anonymous(this=""))
107
139
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
108
140
  else:
109
- subquery = table.build() # pyright: ignore
141
+ subquery = table.build()
110
142
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
111
143
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
112
144
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp