sqlspec 0.25.0__py3-none-any.whl → 0.27.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.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,375 @@
1
+ # pyright: reportPrivateUsage=false
2
+ """JOIN operation mixins.
3
+
4
+ Provides mixins for JOIN operations in SELECT statements.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any, Union, cast
8
+
9
+ from mypy_extensions import trait
10
+ from sqlglot import exp
11
+ from typing_extensions import Self
12
+
13
+ from sqlspec.builder._parsing_utils import parse_table_expression
14
+ from sqlspec.exceptions import SQLBuilderError
15
+ from sqlspec.utils.type_guards import has_query_builder_parameters
16
+
17
+ if TYPE_CHECKING:
18
+ from sqlspec.core.statement import SQL
19
+ from sqlspec.protocols import SQLBuilderProtocol
20
+
21
+ __all__ = ("JoinBuilder", "JoinClauseMixin")
22
+
23
+
24
+ def _handle_sql_object_condition(on: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
25
+ if hasattr(on, "expression") and on.expression is not None:
26
+ if hasattr(on, "parameters"):
27
+ for param_name, param_value in on.parameters.items():
28
+ builder.add_parameter(param_value, name=param_name)
29
+ return cast("exp.Expression", on.expression)
30
+ if hasattr(on, "parameters"):
31
+ for param_name, param_value in on.parameters.items():
32
+ builder.add_parameter(param_value, name=param_name)
33
+ parsed_expr = exp.maybe_parse(on.sql)
34
+ return parsed_expr if parsed_expr is not None else exp.condition(str(on.sql))
35
+
36
+
37
+ def _parse_join_condition(
38
+ builder: "SQLBuilderProtocol", on: Union[str, exp.Expression, "SQL"] | None
39
+ ) -> exp.Expression | None:
40
+ if on is None:
41
+ return None
42
+ if isinstance(on, str):
43
+ return exp.condition(on)
44
+ if hasattr(on, "expression") and hasattr(on, "sql"):
45
+ return _handle_sql_object_condition(on, builder)
46
+ if isinstance(on, exp.Expression):
47
+ return on
48
+ return exp.condition(str(on))
49
+
50
+
51
+ def _handle_query_builder_table(table: Any, alias: str | None, builder: "SQLBuilderProtocol") -> exp.Expression:
52
+ subquery_expression: exp.Expression
53
+ parameters: dict[str, Any] | None = None
54
+ table_parameters = getattr(table, "parameters", None)
55
+ if isinstance(table_parameters, dict):
56
+ parameters = table_parameters
57
+
58
+ if hasattr(table, "_build_final_expression") and callable(table._build_final_expression):
59
+ subquery_expression = cast("exp.Expression", table._build_final_expression(copy=True))
60
+ else:
61
+ subquery_result = table.build()
62
+ sql_text = subquery_result.sql if hasattr(subquery_result, "sql") else str(subquery_result)
63
+ subquery_expression = exp.maybe_parse(sql_text, dialect=builder.dialect) or exp.convert(sql_text)
64
+ if parameters is None and hasattr(subquery_result, "parameters"):
65
+ result_parameters = subquery_result.parameters
66
+ if isinstance(result_parameters, dict):
67
+ parameters = result_parameters
68
+
69
+ if parameters:
70
+ for param_name, param_value in parameters.items():
71
+ builder.add_parameter(param_value, name=param_name)
72
+
73
+ subquery_exp = exp.paren(subquery_expression)
74
+ return exp.alias_(subquery_exp, alias) if alias else subquery_exp
75
+
76
+
77
+ def _parse_join_table(
78
+ builder: "SQLBuilderProtocol", table: str | exp.Expression | Any, alias: str | None
79
+ ) -> exp.Expression:
80
+ if isinstance(table, str):
81
+ return parse_table_expression(table, alias)
82
+ if has_query_builder_parameters(table):
83
+ return _handle_query_builder_table(table, alias, builder)
84
+ if isinstance(table, exp.Expression):
85
+ return table
86
+ return cast("exp.Expression", table)
87
+
88
+
89
+ def _create_join_expression(table_expr: exp.Expression, on_expr: exp.Expression | None, join_type: str) -> exp.Join:
90
+ join_type_upper = join_type.upper()
91
+ if join_type_upper == "INNER":
92
+ return exp.Join(this=table_expr, on=on_expr)
93
+ if join_type_upper == "LEFT":
94
+ return exp.Join(this=table_expr, on=on_expr, side="LEFT")
95
+ if join_type_upper == "RIGHT":
96
+ return exp.Join(this=table_expr, on=on_expr, side="RIGHT")
97
+ if join_type_upper == "FULL":
98
+ return exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
99
+ if join_type_upper == "CROSS":
100
+ return exp.Join(this=table_expr, kind="CROSS")
101
+ msg = f"Unsupported join type: {join_type}"
102
+ raise SQLBuilderError(msg)
103
+
104
+
105
+ def _apply_lateral_modifier(join_expr: exp.Join) -> None:
106
+ current_kind = join_expr.args.get("kind")
107
+ current_side = join_expr.args.get("side")
108
+
109
+ if current_kind == "CROSS":
110
+ join_expr.set("kind", "CROSS LATERAL")
111
+ elif current_kind == "OUTER" and current_side == "FULL":
112
+ join_expr.set("side", "FULL")
113
+ join_expr.set("kind", "OUTER LATERAL")
114
+ elif current_side:
115
+ join_expr.set("kind", f"{current_side} LATERAL")
116
+ join_expr.set("side", None)
117
+ else:
118
+ join_expr.set("kind", "LATERAL")
119
+
120
+
121
+ def build_join_clause(
122
+ builder: "SQLBuilderProtocol",
123
+ table: str | exp.Expression | Any,
124
+ on: Union[str, exp.Expression, "SQL"] | None,
125
+ alias: str | None,
126
+ join_type: str,
127
+ *,
128
+ lateral: bool = False,
129
+ ) -> exp.Join:
130
+ table_expr = _parse_join_table(builder, table, alias)
131
+ on_expr = _parse_join_condition(builder, on)
132
+ join_expr = _create_join_expression(table_expr, on_expr, join_type)
133
+ if lateral:
134
+ _apply_lateral_modifier(join_expr)
135
+ return join_expr
136
+
137
+
138
+ @trait
139
+ class JoinClauseMixin:
140
+ """Mixin providing JOIN clause methods for SELECT builders."""
141
+
142
+ __slots__ = ()
143
+
144
+ # Type annotation for PyRight - this will be provided by the base class
145
+ _expression: exp.Expression | None
146
+
147
+ def join(
148
+ self,
149
+ table: str | exp.Expression | Any,
150
+ on: Union[str, exp.Expression, "SQL"] | None = None,
151
+ alias: str | None = None,
152
+ join_type: str = "INNER",
153
+ lateral: bool = False,
154
+ ) -> Self:
155
+ builder = cast("SQLBuilderProtocol", self)
156
+ if builder._expression is None:
157
+ builder._expression = exp.Select()
158
+ if not isinstance(builder._expression, exp.Select):
159
+ msg = "JOIN clause is only supported for SELECT statements."
160
+ raise SQLBuilderError(msg)
161
+
162
+ if isinstance(table, exp.Join):
163
+ builder._expression = builder._expression.join(table, copy=False)
164
+ return cast("Self", builder)
165
+
166
+ join_expr = build_join_clause(builder, table, on, alias, join_type, lateral=lateral)
167
+ builder._expression = builder._expression.join(join_expr, copy=False)
168
+ return cast("Self", builder)
169
+
170
+ def inner_join(
171
+ self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
172
+ ) -> Self:
173
+ return self.join(table, on, alias, "INNER")
174
+
175
+ def left_join(
176
+ self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
177
+ ) -> Self:
178
+ return self.join(table, on, alias, "LEFT")
179
+
180
+ def right_join(
181
+ self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
182
+ ) -> Self:
183
+ return self.join(table, on, alias, "RIGHT")
184
+
185
+ def full_join(
186
+ self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
187
+ ) -> Self:
188
+ return self.join(table, on, alias, "FULL")
189
+
190
+ def cross_join(self, table: str | exp.Expression | Any, alias: str | None = None) -> Self:
191
+ builder = cast("SQLBuilderProtocol", self)
192
+ if builder._expression is None:
193
+ builder._expression = exp.Select()
194
+ if not isinstance(builder._expression, exp.Select):
195
+ msg = "Cannot add cross join to a non-SELECT expression."
196
+ raise SQLBuilderError(msg)
197
+ table_expr = _parse_join_table(builder, table, alias)
198
+ join_expr = exp.Join(this=table_expr, kind="CROSS")
199
+ builder._expression = builder._expression.join(join_expr, copy=False)
200
+ return cast("Self", builder)
201
+
202
+ def lateral_join(
203
+ self,
204
+ table: str | exp.Expression | Any,
205
+ on: Union[str, exp.Expression, "SQL"] | None = None,
206
+ alias: str | None = None,
207
+ ) -> Self:
208
+ """Create a LATERAL JOIN.
209
+
210
+ Args:
211
+ table: Table, subquery, or table function to join
212
+ on: Optional join condition (for LATERAL JOINs with ON clause)
213
+ alias: Optional alias for the joined table/subquery
214
+
215
+ Returns:
216
+ Self for method chaining
217
+
218
+ Example:
219
+ ```python
220
+ query = (
221
+ sql.select("u.name", "arr.value")
222
+ .from_("users u")
223
+ .lateral_join("UNNEST(u.tags)", alias="arr")
224
+ )
225
+ ```
226
+ """
227
+ return self.join(table, on=on, alias=alias, join_type="INNER", lateral=True)
228
+
229
+ def left_lateral_join(
230
+ self,
231
+ table: str | exp.Expression | Any,
232
+ on: Union[str, exp.Expression, "SQL"] | None = None,
233
+ alias: str | None = None,
234
+ ) -> Self:
235
+ """Create a LEFT LATERAL JOIN.
236
+
237
+ Args:
238
+ table: Table, subquery, or table function to join
239
+ on: Optional join condition
240
+ alias: Optional alias for the joined table/subquery
241
+
242
+ Returns:
243
+ Self for method chaining
244
+ """
245
+ return self.join(table, on=on, alias=alias, join_type="LEFT", lateral=True)
246
+
247
+ def cross_lateral_join(self, table: str | exp.Expression | Any, alias: str | None = None) -> Self:
248
+ """Create a CROSS LATERAL JOIN (no ON condition).
249
+
250
+ Args:
251
+ table: Table, subquery, or table function to join
252
+ alias: Optional alias for the joined table/subquery
253
+
254
+ Returns:
255
+ Self for method chaining
256
+ """
257
+ return self.join(table, on=None, alias=alias, join_type="CROSS", lateral=True)
258
+
259
+
260
+ @trait
261
+ class JoinBuilder:
262
+ """Builder for JOIN operations with fluent syntax.
263
+
264
+ Example:
265
+ ```python
266
+ from sqlspec import sql
267
+
268
+ # sql.left_join_("posts").on("users.id = posts.user_id")
269
+ join_clause = sql.left_join_("posts").on(
270
+ "users.id = posts.user_id"
271
+ )
272
+
273
+ # Or with query builder
274
+ query = (
275
+ sql.select("users.name", "posts.title")
276
+ .from_("users")
277
+ .join(
278
+ sql.left_join_("posts").on(
279
+ "users.id = posts.user_id"
280
+ )
281
+ )
282
+ )
283
+ ```
284
+ """
285
+
286
+ def __init__(self, join_type: str, lateral: bool = False) -> None:
287
+ """Initialize the join builder.
288
+
289
+ Args:
290
+ join_type: Type of join (inner, left, right, full, cross, lateral)
291
+ lateral: Whether this is a LATERAL join
292
+ """
293
+ self._join_type = join_type.upper()
294
+ self._lateral = lateral
295
+ self._table: str | exp.Expression | None = None
296
+ self._condition: exp.Expression | None = None
297
+ self._alias: str | None = None
298
+
299
+ def __call__(self, table: str | exp.Expression, alias: str | None = None) -> Self:
300
+ """Set the table to join.
301
+
302
+ Args:
303
+ table: Table name or expression to join
304
+ alias: Optional alias for the table
305
+
306
+ Returns:
307
+ Self for method chaining
308
+ """
309
+ self._table = table
310
+ self._alias = alias
311
+ return self
312
+
313
+ def on(self, condition: str | exp.Expression) -> exp.Expression:
314
+ """Set the join condition and build the JOIN expression.
315
+
316
+ Args:
317
+ condition: JOIN condition (e.g., "users.id = posts.user_id")
318
+
319
+ Returns:
320
+ Complete JOIN expression
321
+ """
322
+ if not self._table:
323
+ msg = "Table must be set before calling .on()"
324
+ raise SQLBuilderError(msg)
325
+
326
+ # Parse the condition
327
+ condition_expr: exp.Expression
328
+ if isinstance(condition, str):
329
+ parsed: exp.Expression | None = exp.maybe_parse(condition)
330
+ condition_expr = parsed or exp.condition(condition)
331
+ else:
332
+ condition_expr = condition
333
+
334
+ # Build table expression
335
+ table_expr: exp.Expression
336
+ if isinstance(self._table, str):
337
+ table_expr = exp.to_table(self._table)
338
+ if self._alias:
339
+ table_expr = exp.alias_(table_expr, self._alias)
340
+ else:
341
+ table_expr = self._table
342
+ if self._alias:
343
+ table_expr = exp.alias_(table_expr, self._alias)
344
+
345
+ # Create the appropriate join type using same pattern as existing JoinClauseMixin
346
+ if self._join_type in {"INNER JOIN", "INNER", "LATERAL JOIN"}:
347
+ join_expr = exp.Join(this=table_expr, on=condition_expr)
348
+ elif self._join_type in {"LEFT JOIN", "LEFT"}:
349
+ join_expr = exp.Join(this=table_expr, on=condition_expr, side="LEFT")
350
+ elif self._join_type in {"RIGHT JOIN", "RIGHT"}:
351
+ join_expr = exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
352
+ elif self._join_type in {"FULL JOIN", "FULL"}:
353
+ join_expr = exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
354
+ elif self._join_type in {"CROSS JOIN", "CROSS"}:
355
+ # CROSS JOIN doesn't use ON condition
356
+ join_expr = exp.Join(this=table_expr, kind="CROSS")
357
+ else:
358
+ join_expr = exp.Join(this=table_expr, on=condition_expr)
359
+
360
+ if self._lateral or self._join_type == "LATERAL JOIN":
361
+ current_kind = join_expr.args.get("kind")
362
+ current_side = join_expr.args.get("side")
363
+
364
+ if current_kind == "CROSS":
365
+ join_expr.set("kind", "CROSS LATERAL")
366
+ elif current_kind == "OUTER" and current_side == "FULL":
367
+ join_expr.set("side", "FULL") # Keep side
368
+ join_expr.set("kind", "OUTER LATERAL")
369
+ elif current_side:
370
+ join_expr.set("kind", f"{current_side} LATERAL")
371
+ join_expr.set("side", None) # Clear side to avoid duplication
372
+ else:
373
+ join_expr.set("kind", "LATERAL")
374
+
375
+ return join_expr