Skip to content

Populate ADF data lineage + Switch-aware walking on the shared source AST (#62b) - #80

Merged
matthewmoorcroft merged 2 commits into
integration/discoveryfrom
discovery/adf-lineage
Sep 15, 2026
Merged

matthewmoorcroft merged 2 commits into
integration/discoveryfrom
discovery/adf-lineage

Conversation

@matthewmoorcroft

@matthewmoorcroft matthewmoorcroft commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary: Populate ADF source-graph data/control lineage and make discovery traversal cover every Switch and other control-flow branch.

Depends on: #84 and the main re-land of #79.

Blocks: #62, #81, #77, #85.

Current status

This PR is merged into integration/discovery, not main (merge commit 3064f3d). Its checks passed when merged. The implementation exists only on staging and therefore is not available from main.

On staging:

  • src/flowx/sources/adf/dataset_lineage.py — activity_data_assets and resolve_dataset_identity collect all activity input/output references and emit shared DataAsset values using resolved identity first and a structural signature only when supportable; identities are never guessed.
  • src/flowx/sources/adf/discovery_mapping.py — _activity_to_node attaches data_reads/data_writes; _record_invocation records ExecutePipeline targets and waitOnCompletion under neutral invocation properties; adf_definitions_to_source_graphs derives lineage after mapping.
  • src/flowx/discovery_lineage.py — walk_nodes recursively traverses every ContainerNode branch, while build_graph_control_edges, build_graph_data_edges, and build_graph_lineage derive lineage from the shared AST.
  • src/flowx/lineage.py — control_edges_from_calls and data_edges_from_endpoints hold the common dedupe, tier matching, and self-edge rules.
  • src/flowx/discovery_serde.py and src/flowx/ir_serde.py:lineage_to_dict round-trip the per-graph lineage block.

The scope is per-workflow data edges because DataEdge has task endpoints only; control edges may cross workflows.

What needs to change

Re-land this work on main after #84 and #79. Rebase onto the final shared types rather than redefining lineage. Preserve the corrected signature rule: unresolved opaque references without a path anchor must not join merely because an ADF dataset name matches.

Conversion and packaging remain unchanged in this slice.

How to approach

Layer dataset_lineage.py and the ADF population hooks onto the re-landed #79 mapper. Keep discovery traversal source-neutral: ADF-specific extraction belongs in sources/adf, while edge construction stays in discovery_lineage.py/lineage.py.

Acceptance/verification:

  • make test and make fmt pass.
  • Tests cover all Copy/Lookup/activity-level inputs and outputs, distinct parameter bindings, strong identity vs weak signature matching, and no false fallback between distinct resolved identities.
  • ExecutePipeline creates a ControlEdge with source/target/via task/wait/resolved fields.
  • walk_nodes covers If true/false, ForEach/Until body, every Switch case, and Switch default, including empty branches.
  • Discovery serde round-trips populated and empty lineage lists.
  • Existing coverage golden remains unchanged.
  • ADF convert/package and Airflow paths remain untouched and green.

This remains additive discover-only behavior.

matthewmoorcroft and others added 2 commits September 15, 2026 10:38
… AST (#62b)

#62a landed the ADF -> shared SourceGraph mapper and left data lineage
unpopulated. This slice fills it in, entirely additively over the merged #61
substrate and #62a mapper/emitter.

Data lineage: re-home the closed-#36 dataset resolver into a discovery-side
`sources/adf/dataset_lineage.py` that resolves every Copy source/sink, Lookup,
and activity input/output dataset reference (all inputs/outputs, not just index
0) into the shared two-tier `DataAsset`: a physical `identity` (schema.table or
a concrete abfss:// path) when it resolves deterministically -- never guessed --
and an always-present `signature` (the structural path signature, else the
dataset name) for the weak tier.

Control lineage: ExecutePipeline nodes record their callee under a neutral
`invokes_workflow` marker; a new source-neutral `discovery_lineage.py` derives
the graph's `Lineage` block, reusing the shared cores extracted from
`flowx.lineage` (`control_edges_from_calls` / `data_edges_from_endpoints`) so the
tier-matching, self-edge drop, and dedup live in one place. `build_control_edges`
/ `build_data_edges` keep their IR signatures; no source coupling is added to
`lineage.py`.

Switch-aware walking: the discovery walker recurses through every ContainerNode
branch (Switch cases + default, ForEach/Until body, both If branches), and
because the mapper populates reads/writes per node during that same recursion,
nested activities' assets and edges are captured for free.

Data edges are joined within a single SourceGraph, matching the per-workflow
scope of the merged `DataEdge` model (which carries task keys only, no pipeline).
`SourceGraph` gains an additive `lineage` field, serialised additively in
`discovery_serde`; `inventory.json` consumer keys are untouched.

Gates: 1280 unit tests pass (+26 new); ruff format/check and mypy clean.

Co-authored-by: Isaac <no-reply@databricks.com>
… bindings (#62b)

Two blocking correctness fixes in the ADF dataset resolver from cross-review of
PR #80. The lineage.py refactor and Switch recursion were confirmed good and are
untouched.

BLOCKING 1 -- name-only fallback created false lineage edges. An unresolvable
reference used to fall back to the bare dataset name as its signature, so two
unrelated opaque references sharing a name (e.g. two `ds_opaque`) joined into a
bogus DataEdge. The signature is now path-derived only (resolved identity, else
the structural path signature); with neither available it is left empty, and an
empty signature is falsy so `_match_assets` cannot use it as a join key. The
asset is still captured for reporting, it just cannot manufacture a signature
edge. This restores #36's rule: never match on bare dataset name.

BLOCKING 2 -- distinct parameterised references were collapsed. De-dup keyed on
`reference_name` alone merged `ds(tbl=orders)` and `ds(tbl=customers)` -- the same
dataset ref with different params resolving to different physical assets -- into
one, losing an asset. De-dup now keys on `(reference_name, parameter binding)`, so
an identical binding named in both a slot and typeProperties still collapses to
one, but distinct bindings are all retained.

New tests: opaque writer+reader produce no data edge (and carry empty
signatures); two param bindings of one dataset ref resolving to different tables
are both retained; an identical binding in slot + typeProperties still collapses.

Gates: 1283 unit tests pass (+3); ruff format/check and mypy clean. Fixture
lineage edges recomputed: 11 control, 0 data, zero false edges (47 opaque assets
now carry non-joining empty signatures instead of name-based ones).

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft

Copy link
Copy Markdown
Member Author

Addressed both blocking findings in 1e539ba (lineage.py refactor + Switch recursion left as-is, per review).

BLOCKING 1 — name-only fallback → false edges: signatures are now path-derived only (resolved identity, else structural path signature). With neither available the signature is left empty; an empty signature is falsy so _match_assets cannot join on it — the bare dataset name is never a join key again (#36's rule). New test: an opaque writer+reader produce no data edge and carry empty signatures.

BLOCKING 2 — distinct parameterized refs collapsed: de-dup now keys on (reference_name, parameter binding) instead of name alone, so ds(tbl=orders) and ds(tbl=customers) are both retained while an identical binding in both a slot and typeProperties still collapses to one. New tests cover both.

Gates: 1283 unit tests pass (+3), ruff format/check + mypy clean. Recomputed fixture lineage: 11 control edges, 0 data edges, zero false edges — the 47 previously name-signature'd assets now carry non-joining empty signatures.

This pull request and its description were written by Isaac.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant