2626
2727from bigframes import dtypes
2828from bigframes .core import guid
29+ from bigframes .core .compile .sqlglot .expressions import typed_expr
2930import bigframes .core .compile .sqlglot .sqlglot_types as sgt
3031import bigframes .core .local_data as local_data
3132import bigframes .core .schema as bf_schema
@@ -212,7 +213,8 @@ def select(
212213 for id , expr in selected_cols
213214 ]
214215
215- new_expr = self ._encapsulate_as_cte ().select (* selections , append = False )
216+ new_expr , _ = self ._encapsulate_as_cte ()
217+ new_expr = new_expr .select (* selections , append = False )
216218 return SQLGlotIR (expr = new_expr , uid_gen = self .uid_gen )
217219
218220 def order_by (
@@ -247,19 +249,52 @@ def project(
247249 )
248250 for id , expr in projected_cols
249251 ]
250- new_expr = self ._encapsulate_as_cte ().select (* projected_cols_expr , append = True )
252+ new_expr , _ = self ._encapsulate_as_cte ()
253+ new_expr = new_expr .select (* projected_cols_expr , append = True )
251254 return SQLGlotIR (expr = new_expr , uid_gen = self .uid_gen )
252255
253256 def filter (
254257 self ,
255258 condition : sge .Expression ,
256259 ) -> SQLGlotIR :
257260 """Filters the query with the given condition."""
258- new_expr = self ._encapsulate_as_cte ()
261+ new_expr , _ = self ._encapsulate_as_cte ()
259262 return SQLGlotIR (
260263 expr = new_expr .where (condition , append = False ), uid_gen = self .uid_gen
261264 )
262265
266+ def join (
267+ self ,
268+ right : SQLGlotIR ,
269+ join_type : typing .Literal ["inner" , "outer" , "left" , "right" , "cross" ],
270+ conditions : tuple [tuple [typed_expr .TypedExpr , typed_expr .TypedExpr ], ...],
271+ * ,
272+ joins_nulls : bool = True ,
273+ ) -> SQLGlotIR :
274+ """Joins the current query with another SQLGlotIR instance."""
275+ left_select , left_table = self ._encapsulate_as_cte ()
276+ right_select , right_table = right ._encapsulate_as_cte ()
277+
278+ left_ctes = left_select .args .pop ("with" , [])
279+ right_ctes = right_select .args .pop ("with" , [])
280+ merged_ctes = [* left_ctes , * right_ctes ]
281+
282+ join_conditions = [
283+ _join_condition (left , right , joins_nulls ) for left , right in conditions
284+ ]
285+ join_on = sge .And (expressions = join_conditions ) if join_conditions else None
286+
287+ join_type_str = join_type if join_type != "outer" else "full outer"
288+ new_expr = (
289+ sge .Select ()
290+ .select (sge .Star ())
291+ .from_ (left_table )
292+ .join (right_table , on = join_on , join_type = join_type_str )
293+ )
294+ new_expr .set ("with" , sge .With (expressions = merged_ctes ))
295+
296+ return SQLGlotIR (expr = new_expr , uid_gen = self .uid_gen )
297+
263298 def insert (
264299 self ,
265300 destination : bigquery .TableReference ,
@@ -320,12 +355,12 @@ def _explode_single_column(
320355 offset = offset ,
321356 )
322357 selection = sge .Star (replace = [unnested_column_alias .as_ (column )])
358+
323359 # TODO: "CROSS" if not keep_empty else "LEFT"
324360 # TODO: overlaps_with_parent to replace existing column.
325- new_expr = (
326- self ._encapsulate_as_cte ()
327- .select (selection , append = False )
328- .join (unnest_expr , join_type = "CROSS" )
361+ new_expr , _ = self ._encapsulate_as_cte ()
362+ new_expr = new_expr .select (selection , append = False ).join (
363+ unnest_expr , join_type = "CROSS"
329364 )
330365 return SQLGlotIR (expr = new_expr , uid_gen = self .uid_gen )
331366
@@ -373,16 +408,15 @@ def _explode_multiple_columns(
373408 for column in columns
374409 ]
375410 )
376- new_expr = (
377- self ._encapsulate_as_cte ()
378- .select (selection , append = False )
379- .join (unnest_expr , join_type = "CROSS" )
411+ new_expr , _ = self ._encapsulate_as_cte ()
412+ new_expr = new_expr .select (selection , append = False ).join (
413+ unnest_expr , join_type = "CROSS"
380414 )
381415 return SQLGlotIR (expr = new_expr , uid_gen = self .uid_gen )
382416
383417 def _encapsulate_as_cte (
384418 self ,
385- ) -> sge .Select :
419+ ) -> typing . Tuple [ sge .Select , sge . Table ] :
386420 """Transforms a given sge.Select query by pushing its main SELECT statement
387421 into a new CTE and then generates a 'SELECT * FROM new_cte_name'
388422 for the new query."""
@@ -397,11 +431,10 @@ def _encapsulate_as_cte(
397431 alias = new_cte_name ,
398432 )
399433 new_with_clause = sge .With (expressions = [* existing_ctes , new_cte ])
400- new_select_expr = (
401- sge .Select ().select (sge .Star ()).from_ (sge .Table (this = new_cte_name ))
402- )
434+ new_table_expr = sge .Table (this = new_cte_name )
435+ new_select_expr = sge .Select ().select (sge .Star ()).from_ (new_table_expr )
403436 new_select_expr .set ("with" , new_with_clause )
404- return new_select_expr
437+ return new_select_expr , new_table_expr
405438
406439
407440def _literal (value : typing .Any , dtype : dtypes .Dtype ) -> sge .Expression :
@@ -451,3 +484,11 @@ def _table(table: bigquery.TableReference) -> sge.Table:
451484 db = sg .to_identifier (table .dataset_id , quoted = True ),
452485 catalog = sg .to_identifier (table .project , quoted = True ),
453486 )
487+
488+
489+ def _join_condition (
490+ left : typed_expr .TypedExpr ,
491+ right : typed_expr .TypedExpr ,
492+ joins_nulls : bool ,
493+ ) -> typing .Union [sge .EQ , sge .And ]:
494+ return sge .EQ (this = left .expr , expression = right .expr )
0 commit comments