sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -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 +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  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 +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  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 +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +40 -11
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  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 +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -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, Optional
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.mixins import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin, ReturningClauseMixin
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: Optional[str] = None, **kwargs: Any) -> None:
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: Optional[str] = None
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, Optional, Union, cast
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: Optional[exp.Expression]
145
+ _expression: exp.Expression | None
32
146
 
33
147
  def join(
34
148
  self,
35
- table: Union[str, exp.Expression, Any],
36
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
37
- alias: Optional[str] = None,
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
- 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 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: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
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: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
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: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
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: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
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: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
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: exp.Expression
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: Union[str, exp.Expression, Any],
203
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
204
- alias: Optional[str] = None,
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: Union[str, exp.Expression, Any],
230
- on: Optional[Union[str, exp.Expression, "SQL"]] = None,
231
- alias: Optional[str] = None,
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: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
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: Optional[Union[str, exp.Expression]] = None
294
- self._condition: Optional[exp.Expression] = None
295
- self._alias: Optional[str] = None
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: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
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: Union[str, exp.Expression]) -> exp.Expression:
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: Optional[exp.Expression] = exp.maybe_parse(condition)
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