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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/builder/_join.py
ADDED
|
@@ -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
|