nailo2c opened a new pull request, #69234:
URL: https://github.com/apache/airflow/pull/69234

   closes: #47579
   
   ## Summary
   
   `BigQueryInsertJobOperator` running a stored procedure / multi-statement 
query produced a single task event whose inputs and outputs were the 
**aggregation of all child query jobs**, so dataset-level lineage looked like 
every input fed every output. 
   
   This adds a separate OpenLineage QUERY event per BigQuery child job, giving 
accurate per-statement lineage.
   
   + Test Dag run successfully (with connect to BigQuery).
   <img width="1426" height="803" alt="openlineage_af_ui" 
src="https://github.com/user-attachments/assets/c8676220-bcd9-4056-a9ae-19d72f385a3b";
 />
   
   + Open Lineage UI shows `input_table1` -> `query.1` -> `output_table1`
   <img width="1437" height="665" alt="Screenshot 2026-07-02 at 3 42 04 PM" 
src="https://github.com/user-attachments/assets/45714fce-35b9-4904-b248-4f91e7d3fb4f";
 />
   
   ## Before
   
   One COMPLETE event, child datasets flattened together, looks many-to-many:
   
   ```jsonc
   {"eventType": "COMPLETE",
    "job": {"name": "...call_stored_procedure"},
    "inputs":  ["...input_table1", "...input_table2"],
    "outputs": ["...output_table1", "...output_table2"]}   // i1,i2 -> o1,o2 
(misleading)
   ```
   
   ## After
   
   The task event is **unchanged** (still the coarse, task-level summary, 
backward compatible), and each child query is additionally emitted as its own 
event with precise `input -> output` and a `parent` facet linking back to the 
task:
   
   ```jsonc
   // child event 1
   {"eventType": "COMPLETE",
    "job": {"name": "...call_stored_procedure.query.1"},
    "inputs": ["...input_table1"], "outputs": ["...output_table1"],
    "run": {"facets": {"parent": {"job": {"name": "...call_stored_procedure"}},
                       "externalQuery": {"externalQueryId": 
"script_job_..._0"}}}}
   // child event 2  ->  query.2 : input_table2 -> output_table2
   ```
   
   
   ### Test Dag I Used
   
   ```python
   import os
   from datetime import datetime
   
   from airflow.models.dag import DAG
   from airflow.providers.google.cloud.operators.bigquery import (
       BigQueryCreateEmptyDatasetOperator,
       BigQueryInsertJobOperator,
   )
   
   PROJECT_ID = "{{ dag_run.conf.get('project_id', params.project_id) }}"
   DATASET_ID = "{{ dag_run.conf.get('dataset_id', params.dataset_id) }}"
   GCP_CONN_ID = "{{ dag_run.conf.get('gcp_conn_id', params.gcp_conn_id) }}"
   LOCATION = os.environ.get("BQ_OPENLINEAGE_LOCATION", "US")
   
   DAG_ID = "reproduce_issue_47579_bigquery_openlineage"
   ROUTINE_ID = "issue_47579_proc"
   
   INPUT_TABLE_1 = f"`{PROJECT_ID}.{DATASET_ID}.input_table1`"
   INPUT_TABLE_2 = f"`{PROJECT_ID}.{DATASET_ID}.input_table2`"
   OUTPUT_TABLE_1 = f"`{PROJECT_ID}.{DATASET_ID}.output_table1`"
   OUTPUT_TABLE_2 = f"`{PROJECT_ID}.{DATASET_ID}.output_table2`"
   ROUTINE = f"`{PROJECT_ID}.{DATASET_ID}.{ROUTINE_ID}`"
   
   
   with DAG(
       dag_id=DAG_ID,
       schedule=None,
       start_date=datetime(2026, 7, 1),
       catchup=False,
       max_active_runs=1,
       tags=["repro", "bigquery", "openlineage"],
       params={
           "project_id": os.environ.get("BQ_OPENLINEAGE_PROJECT_ID", "SET_ME"),
           "dataset_id": os.environ.get("BQ_OPENLINEAGE_DATASET_ID", 
"issue_47579_openlineage_repro"),
           "gcp_conn_id": os.environ.get("BQ_OPENLINEAGE_CONN_ID", 
"google_cloud_default"),
       },
   ) as dag:
       create_dataset = BigQueryCreateEmptyDatasetOperator(
           task_id="create_dataset",
           project_id=PROJECT_ID,
           dataset_id=DATASET_ID,
           location=LOCATION,
           gcp_conn_id=GCP_CONN_ID,
           if_exists="ignore",
       )
   
       create_input_table1 = BigQueryInsertJobOperator(
           task_id="create_input_table1",
           project_id=PROJECT_ID,
           location=LOCATION,
           gcp_conn_id=GCP_CONN_ID,
           configuration={
               "query": {
                   "query": f"""
                       CREATE OR REPLACE TABLE {INPUT_TABLE_1} AS
                       SELECT 1 AS id, 'only_from_input_table1' AS payload
                   """,
                   "useLegacySql": False,
               }
           },
       )
   
       create_input_table2 = BigQueryInsertJobOperator(
           task_id="create_input_table2",
           project_id=PROJECT_ID,
           location=LOCATION,
           gcp_conn_id=GCP_CONN_ID,
           configuration={
               "query": {
                   "query": f"""
                       CREATE OR REPLACE TABLE {INPUT_TABLE_2} AS
                       SELECT 2 AS id, 'only_from_input_table2' AS payload
                   """,
                   "useLegacySql": False,
               }
           },
       )
   
       create_stored_procedure = BigQueryInsertJobOperator(
           task_id="create_stored_procedure",
           project_id=PROJECT_ID,
           location=LOCATION,
           gcp_conn_id=GCP_CONN_ID,
           configuration={
               "query": {
                   "query": f"""
                       CREATE OR REPLACE PROCEDURE {ROUTINE}()
                       BEGIN
                         CREATE OR REPLACE TABLE {OUTPUT_TABLE_1} AS
                         SELECT id, payload, 'first_ctas' AS marker
                         FROM {INPUT_TABLE_1};
   
                         CREATE OR REPLACE TABLE {OUTPUT_TABLE_2} AS
                         SELECT id, payload, 'second_ctas' AS marker
                         FROM {INPUT_TABLE_2};
                       END
                   """,
                   "useLegacySql": False,
               }
           },
       )
   
       call_stored_procedure = BigQueryInsertJobOperator(
           task_id="call_stored_procedure",
           project_id=PROJECT_ID,
           location=LOCATION,
           gcp_conn_id=GCP_CONN_ID,
           configuration={
               "query": {
                   "query": f"CALL {ROUTINE}();",
                   "useLegacySql": False,
                   "priority": "INTERACTIVE",
               }
           },
       )
   
       create_dataset >> [create_input_table1, create_input_table2] >> 
create_stored_procedure
       create_stored_procedure >> call_stored_procedure
   ```
   
   
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
please
   change below checkbox to `[X]` followed by the name of the tool, uncomment 
the "Generated-by".
   -->
   
   - [X] Yes (please specify the tool below)
   
   Generated-by: Claude Opus 4.8 following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)


-- 
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