sqlspec 0.24.1__py3-none-any.whl → 0.26.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 (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,10 +1,11 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """CTE and set operation mixins.
2
3
 
3
4
  Provides mixins for Common Table Expressions (WITH clause) and
4
5
  set operations (UNION, INTERSECT, EXCEPT).
5
6
  """
6
7
 
7
- from typing import Any, Optional, Union
8
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
8
9
 
9
10
  from mypy_extensions import trait
10
11
  from sqlglot import exp
@@ -12,6 +13,9 @@ from typing_extensions import Self
12
13
 
13
14
  from sqlspec.exceptions import SQLBuilderError
14
15
 
16
+ if TYPE_CHECKING:
17
+ from sqlspec.builder._base import QueryBuilder
18
+
15
19
  __all__ = ("CommonTableExpressionMixin", "SetOperationMixin")
16
20
 
17
21
 
@@ -20,8 +24,10 @@ class CommonTableExpressionMixin:
20
24
  """Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
21
25
 
22
26
  __slots__ = ()
23
- # Type annotation for PyRight - this will be provided by the base class
24
- _expression: Optional[exp.Expression]
27
+
28
+ # Type annotations for PyRight - these will be provided by the base class
29
+ def get_expression(self) -> Optional[exp.Expression]: ...
30
+ def set_expression(self, expression: exp.Expression) -> None: ...
25
31
 
26
32
  _with_ctes: Any # Provided by QueryBuilder
27
33
  dialect: Any # Provided by QueryBuilder
@@ -60,12 +66,14 @@ class CommonTableExpressionMixin:
60
66
  Returns:
61
67
  The current builder instance for method chaining.
62
68
  """
63
- if self._expression is None:
69
+ builder = cast("QueryBuilder", self)
70
+ expression = builder.get_expression()
71
+ if expression is None:
64
72
  msg = "Cannot add WITH clause: expression not initialized."
65
73
  raise SQLBuilderError(msg)
66
74
 
67
- if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
68
- msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
75
+ if not isinstance(expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
76
+ msg = f"Cannot add WITH clause to {type(expression).__name__} expression."
69
77
  raise SQLBuilderError(msg)
70
78
 
71
79
  cte_expr: Optional[exp.Expression] = None
@@ -103,19 +111,17 @@ class CommonTableExpressionMixin:
103
111
  else:
104
112
  cte_alias_expr = exp.alias_(cte_expr, name)
105
113
 
106
- existing_with = self._expression.args.get("with")
114
+ existing_with = expression.args.get("with")
107
115
  if existing_with:
108
116
  existing_with.expressions.append(cte_alias_expr)
109
117
  if recursive:
110
118
  existing_with.set("recursive", recursive)
111
119
  else:
112
- # Only SELECT, INSERT, UPDATE support WITH clauses
113
- if hasattr(self._expression, "with_") and isinstance(
114
- self._expression, (exp.Select, exp.Insert, exp.Update)
115
- ):
116
- self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False)
120
+ if isinstance(expression, (exp.Select, exp.Insert, exp.Update)):
121
+ updated_expression = expression.with_(cte_alias_expr, as_=name, copy=False)
122
+ builder.set_expression(updated_expression)
117
123
  if recursive:
118
- with_clause = self._expression.find(exp.With)
124
+ with_clause = updated_expression.find(exp.With)
119
125
  if with_clause:
120
126
  with_clause.set("recursive", recursive)
121
127
  self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name))
@@ -128,10 +134,12 @@ class SetOperationMixin:
128
134
  """Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
129
135
 
130
136
  __slots__ = ()
131
- # Type annotation for PyRight - this will be provided by the base class
132
- _expression: Optional[exp.Expression]
133
137
 
134
- _parameters: dict[str, Any]
138
+ # Type annotations for PyRight - these will be provided by the base class
139
+ def get_expression(self) -> Optional[exp.Expression]: ...
140
+ def set_expression(self, expression: exp.Expression) -> None: ...
141
+ def set_parameters(self, parameters: "dict[str, Any]") -> None: ...
142
+
135
143
  dialect: Any = None
136
144
 
137
145
  def build(self) -> Any:
@@ -162,7 +170,7 @@ class SetOperationMixin:
162
170
  union_expr = exp.union(left_expr, right_expr, distinct=not all_)
163
171
  new_builder = type(self)()
164
172
  new_builder.dialect = self.dialect
165
- new_builder._expression = union_expr
173
+ cast("QueryBuilder", new_builder).set_expression(union_expr)
166
174
  merged_parameters = dict(left_query.parameters)
167
175
  for param_name, param_value in right_query.parameters.items():
168
176
  if param_name in merged_parameters:
@@ -181,11 +189,11 @@ class SetOperationMixin:
181
189
 
182
190
  right_expr = right_expr.transform(rename_parameter)
183
191
  union_expr = exp.union(left_expr, right_expr, distinct=not all_)
184
- new_builder._expression = union_expr
192
+ cast("QueryBuilder", new_builder).set_expression(union_expr)
185
193
  merged_parameters[new_param_name] = param_value
186
194
  else:
187
195
  merged_parameters[param_name] = param_value
188
- new_builder._parameters = merged_parameters
196
+ new_builder.set_parameters(merged_parameters)
189
197
  return new_builder
190
198
 
191
199
  def intersect(self, other: Any) -> Self:
@@ -210,10 +218,10 @@ class SetOperationMixin:
210
218
  intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
211
219
  new_builder = type(self)()
212
220
  new_builder.dialect = self.dialect
213
- new_builder._expression = intersect_expr
221
+ cast("QueryBuilder", new_builder).set_expression(intersect_expr)
214
222
  merged_parameters = dict(left_query.parameters)
215
223
  merged_parameters.update(right_query.parameters)
216
- new_builder._parameters = merged_parameters
224
+ new_builder.set_parameters(merged_parameters)
217
225
  return new_builder
218
226
 
219
227
  def except_(self, other: Any) -> Self:
@@ -238,8 +246,8 @@ class SetOperationMixin:
238
246
  except_expr = exp.except_(left_expr, right_expr)
239
247
  new_builder = type(self)()
240
248
  new_builder.dialect = self.dialect
241
- new_builder._expression = except_expr
249
+ cast("QueryBuilder", new_builder).set_expression(except_expr)
242
250
  merged_parameters = dict(left_query.parameters)
243
251
  merged_parameters.update(right_query.parameters)
244
- new_builder._parameters = merged_parameters
252
+ new_builder.set_parameters(merged_parameters)
245
253
  return new_builder
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """DELETE operation mixins.
2
3
 
3
4
  Provides mixins for DELETE statement functionality including
@@ -21,8 +22,9 @@ class DeleteFromClauseMixin:
21
22
 
22
23
  __slots__ = ()
23
24
 
24
- # Type annotation for PyRight - this will be provided by the base class
25
- _expression: Optional[exp.Expression]
25
+ # Type annotations for PyRight - these will be provided by the base class
26
+ def get_expression(self) -> Optional[exp.Expression]: ...
27
+ def set_expression(self, expression: exp.Expression) -> None: ...
26
28
 
27
29
  def from_(self, table: str) -> Self:
28
30
  """Set the target table for the DELETE statement.
@@ -33,13 +35,16 @@ class DeleteFromClauseMixin:
33
35
  Returns:
34
36
  The current builder instance for method chaining.
35
37
  """
36
- if self._expression is None:
37
- self._expression = exp.Delete()
38
- if not isinstance(self._expression, exp.Delete):
39
- current_expr_type = type(self._expression).__name__
38
+ current_expr = self.get_expression()
39
+ if current_expr is None:
40
+ self.set_expression(exp.Delete())
41
+ current_expr = self.get_expression()
42
+
43
+ if not isinstance(current_expr, exp.Delete):
44
+ current_expr_type = type(current_expr).__name__
40
45
  msg = f"Base expression for Delete is {current_expr_type}, expected Delete."
41
46
  raise SQLBuilderError(msg)
42
47
 
43
48
  setattr(self, "_table", table)
44
- self._expression.set("this", exp.to_table(table))
49
+ current_expr.set("this", exp.to_table(table))
45
50
  return self
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """INSERT operation mixins.
2
3
 
3
4
  Provides mixins for INSERT statement functionality including
@@ -25,8 +26,9 @@ class InsertIntoClauseMixin:
25
26
 
26
27
  __slots__ = ()
27
28
 
28
- # Type annotation for PyRight - this will be provided by the base class
29
- _expression: Optional[exp.Expression]
29
+ # Type annotations for PyRight - these will be provided by the base class
30
+ def get_expression(self) -> Optional[exp.Expression]: ...
31
+ def set_expression(self, expression: exp.Expression) -> None: ...
30
32
 
31
33
  def into(self, table: str) -> Self:
32
34
  """Set the target table for the INSERT statement.
@@ -40,14 +42,17 @@ class InsertIntoClauseMixin:
40
42
  Returns:
41
43
  The current builder instance for method chaining.
42
44
  """
43
- if self._expression is None:
44
- self._expression = exp.Insert()
45
- if not isinstance(self._expression, exp.Insert):
45
+ current_expr = self.get_expression()
46
+ if current_expr is None:
47
+ self.set_expression(exp.Insert())
48
+ current_expr = self.get_expression()
49
+
50
+ if not isinstance(current_expr, exp.Insert):
46
51
  msg = "Cannot set target table on a non-INSERT expression."
47
52
  raise SQLBuilderError(msg)
48
53
 
49
54
  setattr(self, "_table", table)
50
- self._expression.set("this", exp.to_table(table))
55
+ current_expr.set("this", exp.to_table(table))
51
56
  return self
52
57
 
53
58
 
@@ -57,8 +62,9 @@ class InsertValuesMixin:
57
62
 
58
63
  __slots__ = ()
59
64
 
60
- # Type annotation for PyRight - this will be provided by the base class
61
- _expression: Optional[exp.Expression]
65
+ # Type annotations for PyRight - these will be provided by the base class
66
+ def get_expression(self) -> Optional[exp.Expression]: ...
67
+ def set_expression(self, expression: exp.Expression) -> None: ...
62
68
 
63
69
  _columns: Any # Provided by QueryBuilder
64
70
 
@@ -74,14 +80,17 @@ class InsertValuesMixin:
74
80
 
75
81
  def columns(self, *columns: Union[str, exp.Expression]) -> Self:
76
82
  """Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
77
- if self._expression is None:
78
- self._expression = exp.Insert()
79
- if not isinstance(self._expression, exp.Insert):
83
+ current_expr = self.get_expression()
84
+ if current_expr is None:
85
+ self.set_expression(exp.Insert())
86
+ current_expr = self.get_expression()
87
+
88
+ if not isinstance(current_expr, exp.Insert):
80
89
  msg = "Cannot set columns on a non-INSERT expression."
81
90
  raise SQLBuilderError(msg)
82
91
 
83
92
  # Get the current table from the expression
84
- current_this = self._expression.args.get("this")
93
+ current_this = current_expr.args.get("this")
85
94
  if current_this is None:
86
95
  msg = "Table must be set using .into() before setting columns."
87
96
  raise SQLBuilderError(msg)
@@ -95,11 +104,11 @@ class InsertValuesMixin:
95
104
 
96
105
  # Create Schema object with table and columns
97
106
  schema = exp.Schema(this=table_name, expressions=column_identifiers)
98
- self._expression.set("this", schema)
107
+ current_expr.set("this", schema)
99
108
  # No columns specified - ensure we have just a Table object
100
109
  elif isinstance(current_this, exp.Schema):
101
110
  table_name = current_this.this
102
- self._expression.set("this", exp.Table(this=table_name))
111
+ current_expr.set("this", exp.Table(this=table_name))
103
112
 
104
113
  try:
105
114
  cols = self._columns
@@ -126,9 +135,12 @@ class InsertValuesMixin:
126
135
  Returns:
127
136
  The current builder instance for method chaining.
128
137
  """
129
- if self._expression is None:
130
- self._expression = exp.Insert()
131
- if not isinstance(self._expression, exp.Insert):
138
+ current_expr = self.get_expression()
139
+ if current_expr is None:
140
+ self.set_expression(exp.Insert())
141
+ current_expr = self.get_expression()
142
+
143
+ if not isinstance(current_expr, exp.Insert):
132
144
  msg = "Cannot add values to a non-INSERT expression."
133
145
  raise SQLBuilderError(msg)
134
146
 
@@ -137,8 +149,8 @@ class InsertValuesMixin:
137
149
  msg = "Cannot mix positional values with keyword values."
138
150
  raise SQLBuilderError(msg)
139
151
  try:
140
- _columns = self._columns
141
- if not _columns:
152
+ cols = self._columns
153
+ if not cols:
142
154
  self.columns(*kwargs.keys())
143
155
  except AttributeError:
144
156
  pass
@@ -156,8 +168,8 @@ class InsertValuesMixin:
156
168
  elif len(values) == 1 and hasattr(values[0], "items"):
157
169
  mapping = values[0]
158
170
  try:
159
- _columns = self._columns
160
- if not _columns:
171
+ cols = self._columns
172
+ if not cols:
161
173
  self.columns(*mapping.keys())
162
174
  except AttributeError:
163
175
  pass
@@ -174,9 +186,9 @@ class InsertValuesMixin:
174
186
  row_exprs.append(exp.Placeholder(this=param_name))
175
187
  else:
176
188
  try:
177
- _columns = self._columns
178
- if _columns and len(values) != len(_columns):
179
- msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
189
+ cols = self._columns
190
+ if cols and len(values) != len(cols):
191
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(cols)})."
180
192
  raise SQLBuilderError(msg)
181
193
  except AttributeError:
182
194
  pass
@@ -186,11 +198,9 @@ class InsertValuesMixin:
186
198
  row_exprs.append(v)
187
199
  else:
188
200
  try:
189
- _columns = self._columns
190
- if _columns and i < len(_columns):
191
- column_name = (
192
- str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
193
- )
201
+ cols = self._columns
202
+ if cols and i < len(cols):
203
+ column_name = str(cols[i]).split(".")[-1] if "." in str(cols[i]) else str(cols[i])
194
204
  param_name = self._generate_unique_parameter_name(column_name)
195
205
  else:
196
206
  param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
@@ -200,7 +210,7 @@ class InsertValuesMixin:
200
210
  row_exprs.append(exp.Placeholder(this=param_name))
201
211
 
202
212
  values_expr = exp.Values(expressions=[row_exprs])
203
- self._expression.set("expression", values_expr)
213
+ current_expr.set("expression", values_expr)
204
214
  return self
205
215
 
206
216
  def add_values(self, values: Sequence[Any]) -> Self:
@@ -221,8 +231,9 @@ class InsertFromSelectMixin:
221
231
 
222
232
  __slots__ = ()
223
233
 
224
- # Type annotation for PyRight - this will be provided by the base class
225
- _expression: Optional[exp.Expression]
234
+ # Type annotations for PyRight - these will be provided by the base class
235
+ def get_expression(self) -> Optional[exp.Expression]: ...
236
+ def set_expression(self, expression: exp.Expression) -> None: ...
226
237
 
227
238
  _table: Any # Provided by QueryBuilder
228
239
 
@@ -250,9 +261,12 @@ class InsertFromSelectMixin:
250
261
  except AttributeError:
251
262
  msg = "The target table must be set using .into() before adding values."
252
263
  raise SQLBuilderError(msg)
253
- if self._expression is None:
254
- self._expression = exp.Insert()
255
- if not isinstance(self._expression, exp.Insert):
264
+ current_expr = self.get_expression()
265
+ if current_expr is None:
266
+ self.set_expression(exp.Insert())
267
+ current_expr = self.get_expression()
268
+
269
+ if not isinstance(current_expr, exp.Insert):
256
270
  msg = "Cannot set INSERT source on a non-INSERT expression."
257
271
  raise SQLBuilderError(msg)
258
272
  subquery_parameters = select_builder._parameters
@@ -261,7 +275,7 @@ class InsertFromSelectMixin:
261
275
  self.add_parameter(p_value, name=p_name)
262
276
  select_expr = select_builder._expression
263
277
  if select_expr and isinstance(select_expr, exp.Select):
264
- self._expression.set("expression", select_expr.copy())
278
+ current_expr.set("expression", select_expr.copy())
265
279
  else:
266
280
  msg = "SelectBuilder must have a valid SELECT expression."
267
281
  raise SQLBuilderError(msg)
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """JOIN operation mixins.
2
3
 
3
4
  Provides mixins for JOIN operations in SELECT statements.
@@ -81,16 +82,12 @@ class JoinClauseMixin:
81
82
  self, table: Any, alias: Optional[str], builder: "SQLBuilderProtocol"
82
83
  ) -> exp.Expression:
83
84
  """Handle table parameters that are query builders."""
84
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
85
- table_expr_value = getattr(table, "_expression", None)
86
- if table_expr_value is not None:
87
- subquery_exp = exp.paren(table_expr_value)
88
- else:
89
- subquery_exp = exp.paren(exp.Anonymous(this=""))
85
+ if hasattr(table, "_expression") and table._expression is not None:
86
+ subquery_exp = exp.paren(table._expression)
90
87
  return exp.alias_(subquery_exp, alias) if alias else subquery_exp
91
88
  subquery = table.build()
92
89
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
93
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
90
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
94
91
  return exp.alias_(subquery_exp, alias) if alias else subquery_exp
95
92
 
96
93
  def _parse_on_condition(
@@ -106,28 +103,20 @@ class JoinClauseMixin:
106
103
  return self._handle_sql_object_condition(on, builder)
107
104
  if isinstance(on, exp.Expression):
108
105
  return on
109
- # Last resort - convert to string and parse
110
106
  return exp.condition(str(on))
111
107
 
112
108
  def _handle_sql_object_condition(self, on: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
113
109
  """Handle SQL object conditions with parameter binding."""
114
- expression = getattr(on, "expression", None)
115
- if expression is not None and isinstance(expression, exp.Expression):
116
- # Merge parameters from SQL object into builder
117
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
118
- sql_parameters = getattr(on, "parameters", {})
119
- for param_name, param_value in sql_parameters.items():
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():
120
113
  builder.add_parameter(param_value, name=param_name)
121
- return cast("exp.Expression", expression)
122
- # If expression is None, fall back to parsing the raw SQL
123
- sql_text = getattr(on, "sql", "")
124
- # Merge parameters even when parsing raw SQL
125
- if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
126
- sql_parameters = getattr(on, "parameters", {})
127
- for param_name, param_value in sql_parameters.items():
114
+ return cast("exp.Expression", on.expression)
115
+ if hasattr(on, "parameters"):
116
+ for param_name, param_value in on.parameters.items():
128
117
  builder.add_parameter(param_value, name=param_name)
129
- parsed_expr = exp.maybe_parse(sql_text)
130
- return parsed_expr if parsed_expr is not None else exp.condition(str(sql_text))
118
+ parsed_expr = exp.maybe_parse(on.sql)
119
+ return parsed_expr if parsed_expr is not None else exp.condition(str(on.sql))
131
120
 
132
121
  def _create_join_expression(
133
122
  self, table_expr: exp.Expression, on_expr: Optional[exp.Expression], join_type: str
@@ -194,17 +183,13 @@ class JoinClauseMixin:
194
183
  if isinstance(table, str):
195
184
  table_expr = parse_table_expression(table, alias)
196
185
  elif has_query_builder_parameters(table):
197
- if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
198
- table_expr_value = getattr(table, "_expression", None)
199
- if table_expr_value is not None:
200
- subquery_exp = exp.paren(table_expr_value)
201
- else:
202
- subquery_exp = exp.paren(exp.Anonymous(this=""))
186
+ if hasattr(table, "_expression") and table._expression is not None:
187
+ subquery_exp = exp.paren(table._expression)
203
188
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
204
189
  else:
205
190
  subquery = table.build()
206
191
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
207
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
192
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect))
208
193
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
209
194
  else:
210
195
  table_expr = table