vishalmore90 opened a new issue, #41052:
URL: https://github.com/apache/beam/issues/41052

   ### What happened?
   
   
   
   When joining three or more PCollections in Beam YAML using `type: Join`, 
`_SqlJoinTransform` in `sdks/python/apache_beam/yaml/yaml_join.py` generates 
invalid SQL queries whenever the join graph topology or the dictionary key 
ordering in `input` does not strictly follow a linear chain originating at 
`tables[0]`.
   
   Depending on the input key ordering and equality relationships, 
`_SqlJoinTransform` exhibits two fatal failure modes:
   1. **Missing `ON` clause (`INNER JOIN <table>` without `ON`)**: When an 
intermediate table joins to a subsequent table rather than the first table, its 
join condition is never populated. `_SqlJoinTransform` produces raw SQL 
containing `... INNER JOIN tableB INNER JOIN tableC ON ...`, omitting the 
required `ON` clause for `tableB`. Calcite's SQL parser rejects this 
immediately with `SqlParseException: Encountered "INNER" ... Was expecting one 
of: "ON" ... "USING" ...`.
   2. **Forward table reference in `ON` clause**: When a table is joined before 
one of its dependency tables, `_SqlJoinTransform` generates an `ON` clause 
referencing a table that has not yet been introduced in the `FROM`/`JOIN` 
sequence (e.g. `FROM A INNER JOIN B ON C.id = B.id INNER JOIN C ON ...`). In 
Calcite/standard SQL, relation `C` is not yet in scope when joining `B`, 
causing query validation to fail with `Table 'C' not found` or `Column 'C.id' 
not found`.
   
   #### Code Pointers / Root Cause
   
   In `sdks/python/apache_beam/yaml/yaml_join.py` (lines 263–301):
   
   ```python
     prev_table = tables[0]
     join_conditions = {}
     for i in range(1, len(tables)):
       curr_table = tables[i]
       join_type = generate_join_type(prev_table, curr_table)
       join_conditions[curr_table] = f' {join_type} JOIN {curr_table}'
       prev_table = curr_table
   
     for equality in equalities_in_pairs:
       left, right = equality.keys()
       if left in conditioned and right in conditioned:
         t = tables[max(tables.index(left), tables.index(right))]
         join_conditions[t] = (
             f'{join_conditions[t]} '
             f'AND {left}.{equality[left]} = {right}.{equality[right]}')
       elif left in conditioned:
         join_conditions[right] = (
             f'{join_conditions[right]} '
             f'ON {left}.{equality[left]} = {right}.{equality[right]}')
         conditioned.append(right)
       elif right in conditioned:
         join_conditions[left] = (
             f'{join_conditions[left]} '
             f'ON {left}.{equality[left]} = {right}.{equality[right]}')
         conditioned.append(left)
       else:
         t = tables[max(tables.index(left), tables.index(right))]
         join_conditions[t] = (
             f'{join_conditions[t]} '
             f'ON {left}.{equality[left]} = {right}.{equality[right]}')
         conditioned.append(t)
   
     if fields:
       selects = ', '.join(_parse_fields(tables, fields))
     else:
       selects = '*'
     query = f'SELECT {selects} FROM {first_table}'
     query += ' '.join(condition for condition in join_conditions.values())
     return pcolls | sql_transform_constructor(query)
   ```
   
   1. `tables = list(pcolls.keys())` assumes tables can be joined in the 
arbitrary order defined by Python `dict` iteration order of `input`.
   2. `join_conditions` is pre-populated in fixed `tables[1:]` order with `' 
{join_type} JOIN {curr_table}'`.
   3. If an equality touches `left` and `right` where neither is in 
`conditioned` (`[tables[0]]`), it appends `ON` only to `max(tables.index(left), 
tables.index(right))`. The other table is never given an `ON` clause if 
subsequent conditions also attach to `max(...)`.
   4. As a result, `join_conditions.values()` contains entries without any `ON` 
clause, or references tables that appear later in `tables`.
   
   #### Steps to Reproduce
   
   Consider three related tables: `products`, `orders`, and `line_items`, where 
`orders` links to `line_items` via `order_id`, and `line_items` links to 
`products` via `product_id`.
   
   ```yaml
   pipeline:
     transforms:
       - type: Create
         name: Products
         config:
           elements:
             - product_id: 101
               product_name: "Widget"
   
       - type: Create
         name: Orders
         config:
           elements:
             - order_id: 1
               customer: "Alice"
   
       - type: Create
         name: LineItems
         config:
           elements:
             - order_id: 1
               product_id: 101
               qty: 2
   
       - type: Join
         name: JoinOrdersAndProducts
         input:
           products: Products
           orders: Orders
           line_items: LineItems
         config:
           type: inner
           equalities:
             - orders: order_id
               line_items: order_id
             - line_items: product_id
               products: product_id
   ```
   
   Run this YAML pipeline (or evaluate `_SqlJoinTransform` with these inputs):
   
   **Failure Mode 1 Output:**
   Because `input` keys are `['products', 'orders', 'line_items']`:
   - Initial `conditioned = ['products']`.
   - Condition 1 (`orders.order_id = line_items.order_id`): neither `orders` 
nor `line_items` is in `conditioned`. Hits `else:`, attaches `ON` to 
`line_items` (index 2). `conditioned` becomes `['products', 'line_items']`.
   - Condition 2 (`line_items.product_id = products.product_id`): both are in 
`conditioned`. Hits `left in conditioned and right in conditioned:`, attaches 
`AND` to `line_items`.
   - `join_conditions['orders']` is never assigned an `ON` clause!
   
   Generated SQL:
   ```sql
   SELECT * FROM products INNER JOIN orders  INNER JOIN line_items ON 
orders.order_id = line_items.order_id AND line_items.product_id = 
products.product_id
   ```
   Notice `INNER JOIN orders` has no `ON` clause, causing:
   ```
   
org.apache.beam.vendor.calcite.v1_28_0.org.apache.calcite.sql.parser.SqlParseException:
 Encountered "INNER" at ... Was expecting one of: "ON" ... "USING" ...
   ```
   
   **Failure Mode 2 Output:**
   If the input dictionary order is `['orders', 'products', 'line_items']`:
   Generated SQL:
   ```sql
   SELECT * FROM orders INNER JOIN products ON line_items.product_id = 
products.product_id  INNER JOIN line_items ON orders.order_id = 
line_items.order_id
   ```
   Notice `INNER JOIN products ON line_items.product_id = products.product_id` 
references `line_items` before `line_items` has been joined.
   
   #### Proposed Solution
   
   Instead of assuming `tables = list(pcolls.keys())` defines a valid SQL join 
order:
   1. Build an adjacency graph of tables and join conditions from 
`equalities_in_pairs`.
   2. Traverse the graph starting from `tables[0]` (e.g. via Breadth-First 
Search / topological traversal) to discover tables in an order where every 
newly joined table has an equality predicate connecting it to at least one 
already joined table.
   3. Emit each `JOIN <table_i> ON <conditions connecting table_i to previously 
joined tables>` in that traversal order.
   
   
   ### Issue Priority
   
   Priority: 2 (default / most bugs should be filed as P2)
   
   ### Issue Components
   
   - [ ] Component: Python SDK
   - [ ] Component: Java SDK
   - [ ] Component: Go SDK
   - [ ] Component: Typescript SDK
   - [ ] Component: IO connector
   - [x] Component: Beam YAML
   - [ ] Component: Beam examples
   - [ ] Component: Beam playground
   - [ ] Component: Beam katas
   - [ ] Component: Website
   - [ ] Component: Infrastructure
   - [ ] Component: Spark Runner
   - [ ] Component: Flink Runner
   - [ ] Component: Prism Runner
   - [ ] Component: Hazelcast Jet Runner
   - [ ] Component: Google Cloud Dataflow Runner


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to