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
@@ -1,389 +0,0 @@
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, Optional, 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
- @trait
25
- class JoinClauseMixin:
26
- """Mixin providing JOIN clause methods for SELECT builders."""
27
-
28
- __slots__ = ()
29
-
30
- # Type annotation for PyRight - this will be provided by the base class
31
- _expression: Optional[exp.Expression]
32
-
33
- def join(
34
- self,
35
- table: Union[str, exp.Expression, Any],
36
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
37
- alias: Optional[str] = None,
38
- join_type: str = "INNER",
39
- lateral: bool = False,
40
- ) -> Self:
41
- builder = cast("SQLBuilderProtocol", self)
42
- self._validate_join_context(builder)
43
-
44
- # Handle Join expressions directly (from JoinBuilder.on() calls)
45
- if isinstance(table, exp.Join):
46
- if builder._expression is not None and isinstance(builder._expression, exp.Select):
47
- builder._expression = builder._expression.join(table, copy=False)
48
- return cast("Self", builder)
49
-
50
- table_expr = self._parse_table_expression(table, alias, builder)
51
- on_expr = self._parse_on_condition(on, builder)
52
- join_expr = self._create_join_expression(table_expr, on_expr, join_type)
53
-
54
- if lateral:
55
- self._apply_lateral_modifier(join_expr)
56
-
57
- if builder._expression is not None and isinstance(builder._expression, exp.Select):
58
- builder._expression = builder._expression.join(join_expr, copy=False)
59
- return cast("Self", builder)
60
-
61
- def _validate_join_context(self, builder: "SQLBuilderProtocol") -> None:
62
- """Validate that the join can be applied to the current expression."""
63
- if builder._expression is None:
64
- builder._expression = exp.Select()
65
- if not isinstance(builder._expression, exp.Select):
66
- msg = "JOIN clause is only supported for SELECT statements."
67
- raise SQLBuilderError(msg)
68
-
69
- def _parse_table_expression(
70
- self, table: Union[str, exp.Expression, Any], alias: Optional[str], builder: "SQLBuilderProtocol"
71
- ) -> exp.Expression:
72
- """Parse table parameter into a SQLGlot expression."""
73
- if isinstance(table, str):
74
- return parse_table_expression(table, alias)
75
- if has_query_builder_parameters(table):
76
- return self._handle_query_builder_table(table, alias, builder)
77
- if isinstance(table, exp.Expression):
78
- return table
79
- return cast("exp.Expression", table)
80
-
81
- def _handle_query_builder_table(
82
- self, table: Any, alias: Optional[str], builder: "SQLBuilderProtocol"
83
- ) -> exp.Expression:
84
- """Handle table parameters that are query builders."""
85
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
86
- table_expr_value = getattr(table, "_expression", None)
87
- if table_expr_value is not None:
88
- subquery_exp = exp.paren(table_expr_value)
89
- else:
90
- subquery_exp = exp.paren(exp.Anonymous(this=""))
91
- return exp.alias_(subquery_exp, alias) if alias else subquery_exp
92
- subquery = table.build()
93
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
94
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
95
- return exp.alias_(subquery_exp, alias) if alias else subquery_exp
96
-
97
- def _parse_on_condition(
98
- self, on: Optional[Union[str, exp.Expression, "SQL"]], builder: "SQLBuilderProtocol"
99
- ) -> Optional[exp.Expression]:
100
- """Parse ON condition into a SQLGlot expression."""
101
- if on is None:
102
- return None
103
-
104
- if isinstance(on, str):
105
- return exp.condition(on)
106
- if hasattr(on, "expression") and hasattr(on, "sql"):
107
- return self._handle_sql_object_condition(on, builder)
108
- if isinstance(on, exp.Expression):
109
- return on
110
- # Last resort - convert to string and parse
111
- return exp.condition(str(on))
112
-
113
- def _handle_sql_object_condition(self, on: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
114
- """Handle SQL object conditions with parameter binding."""
115
- expression = getattr(on, "expression", None)
116
- if expression is not None and isinstance(expression, exp.Expression):
117
- # Merge parameters from SQL object into builder
118
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
119
- sql_parameters = getattr(on, "parameters", {})
120
- for param_name, param_value in sql_parameters.items():
121
- builder.add_parameter(param_value, name=param_name)
122
- return cast("exp.Expression", expression)
123
- # If expression is None, fall back to parsing the raw SQL
124
- sql_text = getattr(on, "sql", "")
125
- # Merge parameters even when parsing raw SQL
126
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
127
- sql_parameters = getattr(on, "parameters", {})
128
- for param_name, param_value in sql_parameters.items():
129
- builder.add_parameter(param_value, name=param_name)
130
- parsed_expr = exp.maybe_parse(sql_text)
131
- return parsed_expr if parsed_expr is not None else exp.condition(str(sql_text))
132
-
133
- def _create_join_expression(
134
- self, table_expr: exp.Expression, on_expr: Optional[exp.Expression], join_type: str
135
- ) -> exp.Join:
136
- """Create the appropriate JOIN expression based on join type."""
137
- join_type_upper = join_type.upper()
138
- if join_type_upper == "INNER":
139
- return exp.Join(this=table_expr, on=on_expr)
140
- if join_type_upper == "LEFT":
141
- return exp.Join(this=table_expr, on=on_expr, side="LEFT")
142
- if join_type_upper == "RIGHT":
143
- return exp.Join(this=table_expr, on=on_expr, side="RIGHT")
144
- if join_type_upper == "FULL":
145
- return exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
146
- if join_type_upper == "CROSS":
147
- return exp.Join(this=table_expr, kind="CROSS")
148
- msg = f"Unsupported join type: {join_type}"
149
- raise SQLBuilderError(msg)
150
-
151
- def _apply_lateral_modifier(self, join_expr: exp.Join) -> None:
152
- """Apply LATERAL modifier to the join expression."""
153
- current_kind = join_expr.args.get("kind")
154
- current_side = join_expr.args.get("side")
155
-
156
- if current_kind == "CROSS":
157
- join_expr.set("kind", "CROSS LATERAL")
158
- elif current_kind == "OUTER" and current_side == "FULL":
159
- join_expr.set("side", "FULL") # Keep side
160
- join_expr.set("kind", "OUTER LATERAL")
161
- elif current_side:
162
- join_expr.set("kind", f"{current_side} LATERAL")
163
- join_expr.set("side", None) # Clear side to avoid duplication
164
- else:
165
- join_expr.set("kind", "LATERAL")
166
-
167
- def inner_join(
168
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
169
- ) -> Self:
170
- return self.join(table, on, alias, "INNER")
171
-
172
- def left_join(
173
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
174
- ) -> Self:
175
- return self.join(table, on, alias, "LEFT")
176
-
177
- def right_join(
178
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
179
- ) -> Self:
180
- return self.join(table, on, alias, "RIGHT")
181
-
182
- def full_join(
183
- self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
184
- ) -> Self:
185
- return self.join(table, on, alias, "FULL")
186
-
187
- def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
188
- builder = cast("SQLBuilderProtocol", self)
189
- if builder._expression is None:
190
- builder._expression = exp.Select()
191
- if not isinstance(builder._expression, exp.Select):
192
- msg = "Cannot add cross join to a non-SELECT expression."
193
- raise SQLBuilderError(msg)
194
- table_expr: exp.Expression
195
- if isinstance(table, str):
196
- table_expr = parse_table_expression(table, alias)
197
- elif has_query_builder_parameters(table):
198
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
199
- table_expr_value = getattr(table, "_expression", None)
200
- if table_expr_value is not None:
201
- subquery_exp = exp.paren(table_expr_value)
202
- else:
203
- subquery_exp = exp.paren(exp.Anonymous(this=""))
204
- table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
205
- else:
206
- subquery = table.build()
207
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
208
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
209
- table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
210
- else:
211
- table_expr = table
212
- join_expr = exp.Join(this=table_expr, kind="CROSS")
213
- builder._expression = builder._expression.join(join_expr, copy=False)
214
- return cast("Self", builder)
215
-
216
- def lateral_join(
217
- self,
218
- table: Union[str, exp.Expression, Any],
219
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
220
- alias: Optional[str] = None,
221
- ) -> Self:
222
- """Create a LATERAL JOIN.
223
-
224
- Args:
225
- table: Table, subquery, or table function to join
226
- on: Optional join condition (for LATERAL JOINs with ON clause)
227
- alias: Optional alias for the joined table/subquery
228
-
229
- Returns:
230
- Self for method chaining
231
-
232
- Example:
233
- ```python
234
- query = (
235
- sql.select("u.name", "arr.value")
236
- .from_("users u")
237
- .lateral_join("UNNEST(u.tags)", alias="arr")
238
- )
239
- ```
240
- """
241
- return self.join(table, on=on, alias=alias, join_type="INNER", lateral=True)
242
-
243
- def left_lateral_join(
244
- self,
245
- table: Union[str, exp.Expression, Any],
246
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
247
- alias: Optional[str] = None,
248
- ) -> Self:
249
- """Create a LEFT LATERAL JOIN.
250
-
251
- Args:
252
- table: Table, subquery, or table function to join
253
- on: Optional join condition
254
- alias: Optional alias for the joined table/subquery
255
-
256
- Returns:
257
- Self for method chaining
258
- """
259
- return self.join(table, on=on, alias=alias, join_type="LEFT", lateral=True)
260
-
261
- def cross_lateral_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
262
- """Create a CROSS LATERAL JOIN (no ON condition).
263
-
264
- Args:
265
- table: Table, subquery, or table function to join
266
- alias: Optional alias for the joined table/subquery
267
-
268
- Returns:
269
- Self for method chaining
270
- """
271
- return self.join(table, on=None, alias=alias, join_type="CROSS", lateral=True)
272
-
273
-
274
- @trait
275
- class JoinBuilder:
276
- """Builder for JOIN operations with fluent syntax.
277
-
278
- Example:
279
- ```python
280
- from sqlspec import sql
281
-
282
- # sql.left_join_("posts").on("users.id = posts.user_id")
283
- join_clause = sql.left_join_("posts").on(
284
- "users.id = posts.user_id"
285
- )
286
-
287
- # Or with query builder
288
- query = (
289
- sql.select("users.name", "posts.title")
290
- .from_("users")
291
- .join(
292
- sql.left_join_("posts").on(
293
- "users.id = posts.user_id"
294
- )
295
- )
296
- )
297
- ```
298
- """
299
-
300
- def __init__(self, join_type: str, lateral: bool = False) -> None:
301
- """Initialize the join builder.
302
-
303
- Args:
304
- join_type: Type of join (inner, left, right, full, cross, lateral)
305
- lateral: Whether this is a LATERAL join
306
- """
307
- self._join_type = join_type.upper()
308
- self._lateral = lateral
309
- self._table: Optional[Union[str, exp.Expression]] = None
310
- self._condition: Optional[exp.Expression] = None
311
- self._alias: Optional[str] = None
312
-
313
- def __call__(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
314
- """Set the table to join.
315
-
316
- Args:
317
- table: Table name or expression to join
318
- alias: Optional alias for the table
319
-
320
- Returns:
321
- Self for method chaining
322
- """
323
- self._table = table
324
- self._alias = alias
325
- return self
326
-
327
- def on(self, condition: Union[str, exp.Expression]) -> exp.Expression:
328
- """Set the join condition and build the JOIN expression.
329
-
330
- Args:
331
- condition: JOIN condition (e.g., "users.id = posts.user_id")
332
-
333
- Returns:
334
- Complete JOIN expression
335
- """
336
- if not self._table:
337
- msg = "Table must be set before calling .on()"
338
- raise SQLBuilderError(msg)
339
-
340
- # Parse the condition
341
- condition_expr: exp.Expression
342
- if isinstance(condition, str):
343
- parsed: Optional[exp.Expression] = exp.maybe_parse(condition)
344
- condition_expr = parsed or exp.condition(condition)
345
- else:
346
- condition_expr = condition
347
-
348
- # Build table expression
349
- table_expr: exp.Expression
350
- if isinstance(self._table, str):
351
- table_expr = exp.to_table(self._table)
352
- if self._alias:
353
- table_expr = exp.alias_(table_expr, self._alias)
354
- else:
355
- table_expr = self._table
356
- if self._alias:
357
- table_expr = exp.alias_(table_expr, self._alias)
358
-
359
- # Create the appropriate join type using same pattern as existing JoinClauseMixin
360
- if self._join_type in {"INNER JOIN", "INNER", "LATERAL JOIN"}:
361
- join_expr = exp.Join(this=table_expr, on=condition_expr)
362
- elif self._join_type in {"LEFT JOIN", "LEFT"}:
363
- join_expr = exp.Join(this=table_expr, on=condition_expr, side="LEFT")
364
- elif self._join_type in {"RIGHT JOIN", "RIGHT"}:
365
- join_expr = exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
366
- elif self._join_type in {"FULL JOIN", "FULL"}:
367
- join_expr = exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
368
- elif self._join_type in {"CROSS JOIN", "CROSS"}:
369
- # CROSS JOIN doesn't use ON condition
370
- join_expr = exp.Join(this=table_expr, kind="CROSS")
371
- else:
372
- join_expr = exp.Join(this=table_expr, on=condition_expr)
373
-
374
- if self._lateral or self._join_type == "LATERAL JOIN":
375
- current_kind = join_expr.args.get("kind")
376
- current_side = join_expr.args.get("side")
377
-
378
- if current_kind == "CROSS":
379
- join_expr.set("kind", "CROSS LATERAL")
380
- elif current_kind == "OUTER" and current_side == "FULL":
381
- join_expr.set("side", "FULL") # Keep side
382
- join_expr.set("kind", "OUTER LATERAL")
383
- elif current_side:
384
- join_expr.set("kind", f"{current_side} LATERAL")
385
- join_expr.set("side", None) # Clear side to avoid duplication
386
- else:
387
- join_expr.set("kind", "LATERAL")
388
-
389
- return join_expr