sqlspec 0.26.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 +55 -25
- sqlspec/_typing.py +62 -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 +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +52 -2
- sqlspec/adapters/adbc/driver.py +144 -45
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- 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 +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- 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 +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- 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 +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- 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 +25 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +352 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +138 -43
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- 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 +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +292 -84
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- 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 +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
- sqlspec/adapters/psqlpy/driver.py +101 -31
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- 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 +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +77 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- 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 +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +231 -60
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +95 -161
- sqlspec/driver/_common.py +133 -80
- sqlspec/driver/_sync.py +95 -162
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -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/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- 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 +14 -84
- sqlspec/utils/config_resolver.py +6 -6
- 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 +3 -3
- 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.26.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 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/builder/_insert.py
CHANGED
|
@@ -4,14 +4,15 @@ Provides a fluent interface for building SQL INSERT queries with
|
|
|
4
4
|
parameter binding and validation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Final
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
8
8
|
|
|
9
9
|
from sqlglot import exp
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
from sqlspec.builder._base import QueryBuilder
|
|
13
|
+
from sqlspec.builder._dml import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin
|
|
13
14
|
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
14
|
-
from sqlspec.builder.
|
|
15
|
+
from sqlspec.builder._select import ReturningClauseMixin
|
|
15
16
|
from sqlspec.core.result import SQLResult
|
|
16
17
|
from sqlspec.exceptions import SQLBuilderError
|
|
17
18
|
from sqlspec.utils.type_guards import has_expression_and_sql
|
|
@@ -23,9 +24,6 @@ if TYPE_CHECKING:
|
|
|
23
24
|
__all__ = ("Insert",)
|
|
24
25
|
|
|
25
26
|
ERR_MSG_TABLE_NOT_SET: Final[str] = "The target table must be set using .into() before adding values."
|
|
26
|
-
ERR_MSG_VALUES_COLUMNS_MISMATCH: Final[str] = (
|
|
27
|
-
"Number of values ({values_len}) does not match the number of specified columns ({columns_len})."
|
|
28
|
-
)
|
|
29
27
|
ERR_MSG_INTERNAL_EXPRESSION_TYPE: Final[str] = "Internal error: expression is not an Insert instance as expected."
|
|
30
28
|
ERR_MSG_EXPRESSION_NOT_INITIALIZED: Final[str] = "Internal error: base expression not initialized."
|
|
31
29
|
|
|
@@ -38,7 +36,7 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
38
36
|
|
|
39
37
|
__slots__ = ("_columns", "_table", "_values_added_count")
|
|
40
38
|
|
|
41
|
-
def __init__(self, table:
|
|
39
|
+
def __init__(self, table: str | None = None, **kwargs: Any) -> None:
|
|
42
40
|
"""Initialize INSERT with optional table.
|
|
43
41
|
|
|
44
42
|
Args:
|
|
@@ -47,7 +45,7 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
47
45
|
"""
|
|
48
46
|
super().__init__(**kwargs)
|
|
49
47
|
|
|
50
|
-
self._table:
|
|
48
|
+
self._table: str | None = None
|
|
51
49
|
self._columns: list[str] = []
|
|
52
50
|
self._values_added_count: int = 0
|
|
53
51
|
|
|
@@ -94,81 +92,6 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
|
|
|
94
92
|
"""Get the insert expression (public API)."""
|
|
95
93
|
return self._get_insert_expression()
|
|
96
94
|
|
|
97
|
-
def values(self, *values: Any, **kwargs: Any) -> "Self":
|
|
98
|
-
"""Adds a row of values to the INSERT statement.
|
|
99
|
-
|
|
100
|
-
This method can be called multiple times to insert multiple rows,
|
|
101
|
-
resulting in a multi-row INSERT statement like `VALUES (...), (...)`.
|
|
102
|
-
|
|
103
|
-
Supports:
|
|
104
|
-
- values(val1, val2, val3)
|
|
105
|
-
- values(col1=val1, col2=val2)
|
|
106
|
-
- values(mapping)
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
*values: The values for the row to be inserted. The number of values
|
|
110
|
-
must match the number of columns set by `columns()`, if `columns()` was called
|
|
111
|
-
and specified any non-empty list of columns.
|
|
112
|
-
**kwargs: Column-value pairs for named values.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
The current builder instance for method chaining.
|
|
116
|
-
|
|
117
|
-
Raises:
|
|
118
|
-
SQLBuilderError: If `into()` has not been called to set the table,
|
|
119
|
-
or if `columns()` was called with a non-empty list of columns
|
|
120
|
-
and the number of values does not match the number of specified columns.
|
|
121
|
-
"""
|
|
122
|
-
if not self._table:
|
|
123
|
-
raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
|
|
124
|
-
|
|
125
|
-
if kwargs:
|
|
126
|
-
if values:
|
|
127
|
-
msg = "Cannot mix positional values with keyword values."
|
|
128
|
-
raise SQLBuilderError(msg)
|
|
129
|
-
return self.values_from_dict(kwargs)
|
|
130
|
-
|
|
131
|
-
if len(values) == 1:
|
|
132
|
-
values_0 = values[0]
|
|
133
|
-
if isinstance(values_0, dict):
|
|
134
|
-
return self.values_from_dict(values_0)
|
|
135
|
-
|
|
136
|
-
insert_expr = self.get_insert_expression()
|
|
137
|
-
|
|
138
|
-
if self._columns and len(values) != len(self._columns):
|
|
139
|
-
msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
|
|
140
|
-
raise SQLBuilderError(msg)
|
|
141
|
-
|
|
142
|
-
value_placeholders: list[exp.Expression] = []
|
|
143
|
-
for i, value in enumerate(values):
|
|
144
|
-
if isinstance(value, exp.Expression):
|
|
145
|
-
value_placeholders.append(value)
|
|
146
|
-
elif has_expression_and_sql(value):
|
|
147
|
-
value_expr = extract_sql_object_expression(value, builder=self)
|
|
148
|
-
value_placeholders.append(value_expr)
|
|
149
|
-
else:
|
|
150
|
-
if self._columns and i < len(self._columns):
|
|
151
|
-
column_str = str(self._columns[i])
|
|
152
|
-
column_name = column_str.rsplit(".", maxsplit=1)[-1] if "." in column_str else column_str
|
|
153
|
-
param_name = self.generate_unique_parameter_name(column_name)
|
|
154
|
-
else:
|
|
155
|
-
param_name = self.generate_unique_parameter_name(f"value_{i + 1}")
|
|
156
|
-
_, param_name = self.add_parameter(value, name=param_name)
|
|
157
|
-
value_placeholders.append(exp.Placeholder(this=param_name))
|
|
158
|
-
|
|
159
|
-
tuple_expr = exp.Tuple(expressions=value_placeholders)
|
|
160
|
-
if self._values_added_count == 0:
|
|
161
|
-
insert_expr.set("expression", exp.Values(expressions=[tuple_expr]))
|
|
162
|
-
else:
|
|
163
|
-
current_values = insert_expr.args.get("expression")
|
|
164
|
-
if isinstance(current_values, exp.Values):
|
|
165
|
-
current_values.expressions.append(tuple_expr)
|
|
166
|
-
else:
|
|
167
|
-
insert_expr.set("expression", exp.Values(expressions=[tuple_expr]))
|
|
168
|
-
|
|
169
|
-
self._values_added_count += 1
|
|
170
|
-
return self
|
|
171
|
-
|
|
172
95
|
def values_from_dict(self, data: "Mapping[str, Any]") -> "Self":
|
|
173
96
|
"""Adds a row of values from a dictionary.
|
|
174
97
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Provides mixins for JOIN operations in SELECT statements.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING, Any,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Union, cast
|
|
8
8
|
|
|
9
9
|
from mypy_extensions import trait
|
|
10
10
|
from sqlglot import exp
|
|
@@ -21,6 +21,120 @@ if TYPE_CHECKING:
|
|
|
21
21
|
__all__ = ("JoinBuilder", "JoinClauseMixin")
|
|
22
22
|
|
|
23
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
|
+
|
|
24
138
|
@trait
|
|
25
139
|
class JoinClauseMixin:
|
|
26
140
|
"""Mixin providing JOIN clause methods for SELECT builders."""
|
|
@@ -28,180 +142,68 @@ class JoinClauseMixin:
|
|
|
28
142
|
__slots__ = ()
|
|
29
143
|
|
|
30
144
|
# Type annotation for PyRight - this will be provided by the base class
|
|
31
|
-
_expression:
|
|
145
|
+
_expression: exp.Expression | None
|
|
32
146
|
|
|
33
147
|
def join(
|
|
34
148
|
self,
|
|
35
|
-
table:
|
|
36
|
-
on:
|
|
37
|
-
alias:
|
|
149
|
+
table: str | exp.Expression | Any,
|
|
150
|
+
on: Union[str, exp.Expression, "SQL"] | None = None,
|
|
151
|
+
alias: str | None = None,
|
|
38
152
|
join_type: str = "INNER",
|
|
39
153
|
lateral: bool = False,
|
|
40
154
|
) -> Self:
|
|
41
155
|
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
156
|
if builder._expression is None:
|
|
64
157
|
builder._expression = exp.Select()
|
|
65
158
|
if not isinstance(builder._expression, exp.Select):
|
|
66
159
|
msg = "JOIN clause is only supported for SELECT statements."
|
|
67
160
|
raise SQLBuilderError(msg)
|
|
68
161
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 table._expression is not None:
|
|
86
|
-
subquery_exp = exp.paren(table._expression)
|
|
87
|
-
return exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
88
|
-
subquery = table.build()
|
|
89
|
-
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
90
|
-
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
|
|
91
|
-
return exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
92
|
-
|
|
93
|
-
def _parse_on_condition(
|
|
94
|
-
self, on: Optional[Union[str, exp.Expression, "SQL"]], builder: "SQLBuilderProtocol"
|
|
95
|
-
) -> Optional[exp.Expression]:
|
|
96
|
-
"""Parse ON condition into a SQLGlot expression."""
|
|
97
|
-
if on is None:
|
|
98
|
-
return None
|
|
99
|
-
|
|
100
|
-
if isinstance(on, str):
|
|
101
|
-
return exp.condition(on)
|
|
102
|
-
if hasattr(on, "expression") and hasattr(on, "sql"):
|
|
103
|
-
return self._handle_sql_object_condition(on, builder)
|
|
104
|
-
if isinstance(on, exp.Expression):
|
|
105
|
-
return on
|
|
106
|
-
return exp.condition(str(on))
|
|
107
|
-
|
|
108
|
-
def _handle_sql_object_condition(self, on: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
|
|
109
|
-
"""Handle SQL object conditions with parameter binding."""
|
|
110
|
-
if hasattr(on, "expression") and on.expression is not None:
|
|
111
|
-
if hasattr(on, "parameters"):
|
|
112
|
-
for param_name, param_value in on.parameters.items():
|
|
113
|
-
builder.add_parameter(param_value, name=param_name)
|
|
114
|
-
return cast("exp.Expression", on.expression)
|
|
115
|
-
if hasattr(on, "parameters"):
|
|
116
|
-
for param_name, param_value in on.parameters.items():
|
|
117
|
-
builder.add_parameter(param_value, name=param_name)
|
|
118
|
-
parsed_expr = exp.maybe_parse(on.sql)
|
|
119
|
-
return parsed_expr if parsed_expr is not None else exp.condition(str(on.sql))
|
|
120
|
-
|
|
121
|
-
def _create_join_expression(
|
|
122
|
-
self, table_expr: exp.Expression, on_expr: Optional[exp.Expression], join_type: str
|
|
123
|
-
) -> exp.Join:
|
|
124
|
-
"""Create the appropriate JOIN expression based on join type."""
|
|
125
|
-
join_type_upper = join_type.upper()
|
|
126
|
-
if join_type_upper == "INNER":
|
|
127
|
-
return exp.Join(this=table_expr, on=on_expr)
|
|
128
|
-
if join_type_upper == "LEFT":
|
|
129
|
-
return exp.Join(this=table_expr, on=on_expr, side="LEFT")
|
|
130
|
-
if join_type_upper == "RIGHT":
|
|
131
|
-
return exp.Join(this=table_expr, on=on_expr, side="RIGHT")
|
|
132
|
-
if join_type_upper == "FULL":
|
|
133
|
-
return exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
|
|
134
|
-
if join_type_upper == "CROSS":
|
|
135
|
-
return exp.Join(this=table_expr, kind="CROSS")
|
|
136
|
-
msg = f"Unsupported join type: {join_type}"
|
|
137
|
-
raise SQLBuilderError(msg)
|
|
138
|
-
|
|
139
|
-
def _apply_lateral_modifier(self, join_expr: exp.Join) -> None:
|
|
140
|
-
"""Apply LATERAL modifier to the join expression."""
|
|
141
|
-
current_kind = join_expr.args.get("kind")
|
|
142
|
-
current_side = join_expr.args.get("side")
|
|
143
|
-
|
|
144
|
-
if current_kind == "CROSS":
|
|
145
|
-
join_expr.set("kind", "CROSS LATERAL")
|
|
146
|
-
elif current_kind == "OUTER" and current_side == "FULL":
|
|
147
|
-
join_expr.set("side", "FULL") # Keep side
|
|
148
|
-
join_expr.set("kind", "OUTER LATERAL")
|
|
149
|
-
elif current_side:
|
|
150
|
-
join_expr.set("kind", f"{current_side} LATERAL")
|
|
151
|
-
join_expr.set("side", None) # Clear side to avoid duplication
|
|
152
|
-
else:
|
|
153
|
-
join_expr.set("kind", "LATERAL")
|
|
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)
|
|
154
169
|
|
|
155
170
|
def inner_join(
|
|
156
|
-
self, table:
|
|
171
|
+
self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
|
|
157
172
|
) -> Self:
|
|
158
173
|
return self.join(table, on, alias, "INNER")
|
|
159
174
|
|
|
160
175
|
def left_join(
|
|
161
|
-
self, table:
|
|
176
|
+
self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
|
|
162
177
|
) -> Self:
|
|
163
178
|
return self.join(table, on, alias, "LEFT")
|
|
164
179
|
|
|
165
180
|
def right_join(
|
|
166
|
-
self, table:
|
|
181
|
+
self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
|
|
167
182
|
) -> Self:
|
|
168
183
|
return self.join(table, on, alias, "RIGHT")
|
|
169
184
|
|
|
170
185
|
def full_join(
|
|
171
|
-
self, table:
|
|
186
|
+
self, table: str | exp.Expression | Any, on: Union[str, exp.Expression, "SQL"], alias: str | None = None
|
|
172
187
|
) -> Self:
|
|
173
188
|
return self.join(table, on, alias, "FULL")
|
|
174
189
|
|
|
175
|
-
def cross_join(self, table:
|
|
190
|
+
def cross_join(self, table: str | exp.Expression | Any, alias: str | None = None) -> Self:
|
|
176
191
|
builder = cast("SQLBuilderProtocol", self)
|
|
177
192
|
if builder._expression is None:
|
|
178
193
|
builder._expression = exp.Select()
|
|
179
194
|
if not isinstance(builder._expression, exp.Select):
|
|
180
195
|
msg = "Cannot add cross join to a non-SELECT expression."
|
|
181
196
|
raise SQLBuilderError(msg)
|
|
182
|
-
table_expr
|
|
183
|
-
if isinstance(table, str):
|
|
184
|
-
table_expr = parse_table_expression(table, alias)
|
|
185
|
-
elif has_query_builder_parameters(table):
|
|
186
|
-
if hasattr(table, "_expression") and table._expression is not None:
|
|
187
|
-
subquery_exp = exp.paren(table._expression)
|
|
188
|
-
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
189
|
-
else:
|
|
190
|
-
subquery = table.build()
|
|
191
|
-
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
192
|
-
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
|
|
193
|
-
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
194
|
-
else:
|
|
195
|
-
table_expr = table
|
|
197
|
+
table_expr = _parse_join_table(builder, table, alias)
|
|
196
198
|
join_expr = exp.Join(this=table_expr, kind="CROSS")
|
|
197
199
|
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
198
200
|
return cast("Self", builder)
|
|
199
201
|
|
|
200
202
|
def lateral_join(
|
|
201
203
|
self,
|
|
202
|
-
table:
|
|
203
|
-
on:
|
|
204
|
-
alias:
|
|
204
|
+
table: str | exp.Expression | Any,
|
|
205
|
+
on: Union[str, exp.Expression, "SQL"] | None = None,
|
|
206
|
+
alias: str | None = None,
|
|
205
207
|
) -> Self:
|
|
206
208
|
"""Create a LATERAL JOIN.
|
|
207
209
|
|
|
@@ -226,9 +228,9 @@ class JoinClauseMixin:
|
|
|
226
228
|
|
|
227
229
|
def left_lateral_join(
|
|
228
230
|
self,
|
|
229
|
-
table:
|
|
230
|
-
on:
|
|
231
|
-
alias:
|
|
231
|
+
table: str | exp.Expression | Any,
|
|
232
|
+
on: Union[str, exp.Expression, "SQL"] | None = None,
|
|
233
|
+
alias: str | None = None,
|
|
232
234
|
) -> Self:
|
|
233
235
|
"""Create a LEFT LATERAL JOIN.
|
|
234
236
|
|
|
@@ -242,7 +244,7 @@ class JoinClauseMixin:
|
|
|
242
244
|
"""
|
|
243
245
|
return self.join(table, on=on, alias=alias, join_type="LEFT", lateral=True)
|
|
244
246
|
|
|
245
|
-
def cross_lateral_join(self, table:
|
|
247
|
+
def cross_lateral_join(self, table: str | exp.Expression | Any, alias: str | None = None) -> Self:
|
|
246
248
|
"""Create a CROSS LATERAL JOIN (no ON condition).
|
|
247
249
|
|
|
248
250
|
Args:
|
|
@@ -290,11 +292,11 @@ class JoinBuilder:
|
|
|
290
292
|
"""
|
|
291
293
|
self._join_type = join_type.upper()
|
|
292
294
|
self._lateral = lateral
|
|
293
|
-
self._table:
|
|
294
|
-
self._condition:
|
|
295
|
-
self._alias:
|
|
295
|
+
self._table: str | exp.Expression | None = None
|
|
296
|
+
self._condition: exp.Expression | None = None
|
|
297
|
+
self._alias: str | None = None
|
|
296
298
|
|
|
297
|
-
def __call__(self, table:
|
|
299
|
+
def __call__(self, table: str | exp.Expression, alias: str | None = None) -> Self:
|
|
298
300
|
"""Set the table to join.
|
|
299
301
|
|
|
300
302
|
Args:
|
|
@@ -308,7 +310,7 @@ class JoinBuilder:
|
|
|
308
310
|
self._alias = alias
|
|
309
311
|
return self
|
|
310
312
|
|
|
311
|
-
def on(self, condition:
|
|
313
|
+
def on(self, condition: str | exp.Expression) -> exp.Expression:
|
|
312
314
|
"""Set the join condition and build the JOIN expression.
|
|
313
315
|
|
|
314
316
|
Args:
|
|
@@ -324,7 +326,7 @@ class JoinBuilder:
|
|
|
324
326
|
# Parse the condition
|
|
325
327
|
condition_expr: exp.Expression
|
|
326
328
|
if isinstance(condition, str):
|
|
327
|
-
parsed:
|
|
329
|
+
parsed: exp.Expression | None = exp.maybe_parse(condition)
|
|
328
330
|
condition_expr = parsed or exp.condition(condition)
|
|
329
331
|
else:
|
|
330
332
|
condition_expr = condition
|