Populate ADF data lineage + Switch-aware walking on the shared source AST (#62b) - #80
Conversation
… 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>
|
Addressed both blocking findings in 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 BLOCKING 2 — distinct parameterized refs collapsed: de-dup now keys on 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. |
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:
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:
This remains additive discover-only behavior.