Skip to content

Backport #31654 and #32214 to 2.0: Databricks DLT SQL lineage and self-referencing lineage - #32395

Merged
mohittilala merged 7 commits into
2.0from
backport/databricks-dlt-lineage-2.0
Sep 3, 2026
Merged

mohittilala merged 7 commits into
2.0from
backport/databricks-dlt-lineage-2.0

Conversation

@mohittilala

Copy link
Copy Markdown
Contributor

Describe your changes:

Backport of two merged Databricks lineage fixes to 2.0.

Both cherry-picks applied cleanly with no conflicts. Every connector and test file is byte-identical to main.

The one intentional difference is that metadata.py keeps 2.0's two existing # noqa: PLC0415 suppressions. 2.0's ruff config enables PLC and main's does not, so taking main's version verbatim would fail 2.0's lint. Those lines are untouched by either fix.

Type of change:

  • Bug fix

High-level design:

N/A, backport with no new design.

Tests:

Use cases covered

Same as the source PRs. Verified on this branch rather than assumed:

  • 160 unit tests pass across the Databricks pipeline and Unity Catalog suites, matching main.
  • The real-workspace parse matrix scores 30/30 against a hand-written oracle, resolving the same 46 files across 10 live DLT pipelines as main, covering every library shape the pipelines API accepts (exact file, bare directory, trailing slash, **, mixed entries).
  • A full ingestion into a clean OpenMetadata, using a fresh timestamped service that did not exist beforehand, wrote 34 lineage edges, identical to main. Zero OpenSearch errors.

Backend integration tests

  • Not applicable (no backend API changes).

Ingestion integration tests

  • Not applicable. Covered by the source PRs and re-verified here against a live Databricks workspace.

Playwright (UI) tests

  • Not applicable (no UI changes).

Manual testing performed

  1. Cherry-picked Fixes 32206: stop linking a table to itself from the Databricks system lineage tables #32214 then Fixes 31278: Extract lineage from SQL-defined Databricks DLT pipelines #31654, in that order, since Fixes 31278: Extract lineage from SQL-defined Databricks DLT pipelines #31654 modifies a test file Fixes 32206: stop linking a table to itself from the Databricks system lineage tables #32214 creates.
  2. Diffed every touched file against main to confirm no drift beyond the PLC0415 note above.
  3. Ran the Databricks and Unity Catalog unit suites on this branch.
  4. Ran the parse matrix against a real Databricks workspace using this branch's code.
  5. Ran a full ingestion into OpenMetadata against a fresh service and counted the edges written.

UI screen recording / screenshots:

Not applicable.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR is linked to the source PRs above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: not applicable.
  • For UI changes: not applicable.
  • I have added tests and listed them above.

…m lineage tables (#32214)

* Fixes #32206: stop linking a table to itself from the Databricks system lineage tables

system.access.table_lineage records table access rather than derivation, so a
streaming or CDC write legitimately names its target table as its own source. The
Databricks Pipeline and Unity Catalog connectors both passed those rows straight
through, producing an edge from a table to itself that renders as a loop and
carries no information.

Skip rows where source and target are the same table, matching the guard the SQL
DLT parser already applies.

* fix(unitycatalog): skip self-referencing rows in the column lineage cache too

The table pair they belong to is dropped, so the cached columns can never be
read. They only grow column_lineage_map and inflate the cached-lineage counts
in the log.
#31654)

* Fixes 31278: Extract lineage from SQL-defined Databricks DLT pipelines

SQL DLT transformations parsed to nothing because kafka_parser only
understood the Python DLT API, so those pipelines produced no lineage.
Parse CREATE [OR REFRESH] MATERIALIZED VIEW / STREAMING TABLE through
the existing LineageParser, resolve qualified upstreams against their
own catalog and schema, read `file` libraries, and recurse into
workspace subdirectories.

* refactor(databricks): dispatch DLT parsing through a language registry

Adding SQL support as a fallback inside extract_dlt_table_dependencies
left an if/elif chain that dispatched on failure rather than language,
against the registry convention in docs/design-patterns.md.

Introduce dlt_parsers.py with a DltSourceParser protocol and registry.
Python and SQL each declare handles/extract and an explicit priority, so
a Python notebook embedding SQL strings is not misread and a new language
needs no dispatch edit. Move the parse-result dataclasses to models.py and
reduce kafka_parser.py to Kafka configuration.

Fix three Databricks SQL shapes found while probing real constructs:
APPLY CHANGES INTO lost its source, the LIVE. prefix was resolved as a
schema, and table-valued functions were returned as tables. Also use the
previously dead get_pipeline_libraries helper instead of a duplicated
inline copy.

* fix(databricks): address PR review on DLT source discovery

Glob includes were only normalised for the `/**` form, so `*.sql` and
`**/*.sql` kept their wildcard and were exported as literal paths, missing
every transformation. Reduce any include to the directory before its first
wildcard, always ending in a slash.

Stripping the LIVE. prefix could collapse two references onto one dataset,
so de-duplicate upstreams while preserving order. Drop table-valued
functions only when the statement invokes them, so a dataset named `range`
still resolves. Strip whitespace and stray dots when qualifying a name.
Remove the dead DLT_TABLE_PATTERN and the unused get_pipeline_libraries
client parameter.

* refactor(databricks): model DLT references and simplify parser dispatch

_qualify_dlt_table_name returned a bare (catalog, schema, table) tuple, so
callers had to know what each position meant. Return a DLTTableReference
instead, alongside the other connector models.

Replace the parser registry, the Protocol priority field and the per-call
sort with an explicit ordered DLT_PARSERS list. Two implementations did not
justify the indirection, and the previous approach also misused
enum_register, which everywhere else keys functions by an enum value rather
than classes by an ad-hoc string. Order is now stated where the list is
defined, and a test asserts it rather than assuming it.

Bound the entity caches with LRUCache, which CLAUDE.md requires and the
sibling Databricks ownership source already uses. Typing the cache surfaced
that fqn.build can return None and the old code cached under a None key, so
collapse the two duplicated cache blocks into a single _lookup_table helper
that guards it once.

Build the source through the real create() in tests instead of assigning its
internals. The hand-assigned caches had already drifted from production once
without any test failing. Cache bounds, eviction, miss caching and the None
guard are now asserted against the real object.

* fix(databricks): select only the files a DLT glob actually matches

glob_base_directory reduced an include to its base directory and the caller
then expanded that whole tree, so a selective include such as
/transformations/*.sql pulled in unrelated files and attributed their
lineage to the pipeline. That is worse than the wildcard bug it replaced,
because the edges are wrong rather than absent.

Model a library entry as DLTLibrarySource, carrying the directory to list
and the pattern its contents must match. Recurse only for **, and check
every candidate with glob_matches, where * stays inside a path segment and
** spans them. fnmatch is unsuitable because its * also crosses /.

Tests now assert which files an include selects rather than which directory
it reduces to, which is what the earlier assertion missed.

* fix(databricks): keep library entries modelled and treat ? as a wildcard

get_pipeline_libraries returns DLTLibrarySource, but the spec.configuration
and spec.development source_path fallback still appended a raw string, so
iterating the list hit AttributeError on library.is_directory and lineage
died for pipelines that use those fields.

glob_base_directory only looked for *, while glob_matches also honours ?, so
an include such as /transformations/file_?.sql was treated as a concrete
path and sent to the export API literally. Both now share GLOB_WILDCARDS so
they cannot disagree again.

* fix(databricks): expand a pattern-less library directory in full

A configuration or development source_path produces a library with no
pattern, and treating that as non-recursive skipped every nested
transformation. No pattern means no filter, so the whole tree is in scope.

An include that names a bare directory was also stored as its own pattern,
which then matched nothing and dropped the directory entirely. Only attach
a pattern when the include actually contains a wildcard.

Cover every spec.libraries shape end to end so the reduction and the
matching cannot drift apart again.

* fix(databricks): state directory-ness instead of inferring it from the path

is_directory was read off a trailing slash, so a source_path that Databricks
spells without one was exported as a file and its transformations never
listed. The producer knows what each entry is, so it now says so: notebook
and file entries are files, a wildcard include is always a directory, and a
source_path is normalised to a directory.

is_recursive only looked for **, so a wildcard in a directory segment such as
/tx/2024_?/file.sql reduced to /tx/ and then refused to descend, selecting
nothing. Decide from the part of the pattern below the base directory, where
either ** or a / means traversal is required.

* fix(databricks): let the workspace settle an ambiguous glob include

A glob include with neither a wildcard nor a trailing slash may name a file
or a directory, and the string cannot tell them apart: assuming a file leaves
a directory unlisted, assuming a directory swallows a concrete .sql. Model
the gap instead, with is_directory None meaning the spec did not say, and
resolve it by listing the path. Anything that lists is a directory, anything
that does not is read as a single source.

* fix(databricks): read the legacy and private DLT create forms

CREATE LIVE TABLE, CREATE STREAMING LIVE TABLE and the PRIVATE modifier are
spellings DLT still accepts but the query parser cannot read. It falls back to a
bare command and reports no tables, so the dataset was dropped with neither an
error nor a warning.

Rewrite them to the modern equivalent before parsing, anchored to the CREATE
clause so a dataset whose own name contains "live" is left alone.

* refactor(databricks): drop the glob machinery Databricks cannot produce

The pipelines API accepts only an exact file, a directory, or a directory with a
trailing **, and rejects every other wildcard form outright. The pattern matcher,
the wildcard set and the recursive flag were therefore unreachable, along with the
tests asserting on them.

A library is now just a file or a directory taken in full.

* fix(databricks): describe library discovery as it behaves and lift the depth cap

The get_pipeline_libraries docstring still promised pattern filtering that no
longer exists, and the expansion cap of 5 silently dropped transformations nested
deeper than that, which costs the lineage depending on them.

Correct the docstring and raise the cap to 20. A directory that is not there costs
nothing to look for.

* fix(databricks): use the real lookup cache in the self-referencing lineage test

The test stubbed _table_lookup_cache with a plain dict, which satisfied main's
implementation but not this branch's LRUCache. The lookup raised, the exception
was swallowed, and the two tests expecting an edge failed once main was merged in.

* fix(databricks): read a null libraries value and quieten per-file logging

The libraries key can be present and null, and testing for the key rather than
reading it made len() raise before the source paths were collected, which cost the
pipeline its lineage. Per-path discovery also logged at INFO, which buries the run
summary now that expansion is recursive.
@mohittilala
mohittilala requested a review from a team as a code owner September 1, 2026 16:50
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Sep 1, 2026
@mohittilala mohittilala added the skip-pr-checks Bypass PR metadata validation check label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@mohittilala mohittilala self-assigned this Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit c991a5332e3b65682affaecc6b47803064d4c6e4 in Playwright run 33643608230, attempt 1.

✅ 110 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 55m 39s

⏱️ Max setup 3m 9s · max shard execution 12m 12s · max shard-job elapsed before upload 20m 13s · reporting 3s

🌐 208.66 requests/attempt · 1.79 app boots/UI scenario · 0.00% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 208.66 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 1.79 per UI scenario (216 boots / 121 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 46 0 0 0 0 0
✅ Shard ingestion-01 34 0 0 0 0 0
✅ Shard ingestion-02 30 0 0 0 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

mohittilala and others added 3 commits September 2, 2026 19:55
…e workspace root (#32404)

* Fixes #32397: stop a wildcard include with no directory reducing to the workspace root

glob_base_directory truncates an include at its first wildcard and falls back to the
parent directory. With nothing before the wildcard there is no parent, so "**" and "*"
reduced to "/" and the expansion walked the entire workspace to max_depth.

Return such an include unchanged so it resolves to nothing, which is far cheaper than
listing everything.

* fix(databricks): leave a first-segment wildcard include alone as well

A base of "/" took the early return and handed back the workspace root, so
"/*.sql" still reduced to "/" and expansion listed everything. The docstring also
promised a real directory fallback that no longer holds for these forms.

Return the include unchanged whenever the only directory above it is the root.
2.0 enables the pylint convention rules that main does not, so the imports kept
inside the SQL parser to avoid paying for the lineage stack up front fail its
checkstyle. Suppress them the way the connector already does elsewhere.
@gitar-bot

gitar-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Backport of Databricks DLT SQL lineage fixes to 2.0: adds SQL-defined pipeline parsing and filters self-referencing lineage edges. Glob wildcard handling corrected to prevent bare includes from reducing to workspace root. All 160 unit tests pass and live workspace ingestion yields 34 lineage edges matching main. No issues found.

✅ 1 resolved
Edge Case: glob_base_directory maps a bare wildcard include to workspace root

📄 ingestion/src/metadata/ingestion/source/pipeline/databrickspipeline/kafka_parser.py:198-212
In kafka_parser.py, glob_base_directory falls back to base.rsplit('/', 1)[0] + '/' when the truncated base does not end in '/'. For an include with no path segment before the wildcard (e.g. '**' or '*'), base becomes '' and the result is '/', which _expand_workspace_directory would then walk from the workspace root down to max_depth=20. The comments note the pipelines API should never emit such a form, so this is defensive-only, but a stray/malformed include would trigger a broad and expensive listing rather than being skipped. Consider returning the include unchanged (or skipping) when the computed base is empty.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@mohittilala
mohittilala merged commit 72ff5f2 into 2.0 Sep 3, 2026
101 checks passed
@mohittilala
mohittilala deleted the backport/databricks-dlt-lineage-2.0 branch September 3, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants