fix(ingestion/grafana): don't break SQL parsing for quoted template variables - #19349
Open
daha wants to merge 1 commit into
Open
fix(ingestion/grafana): don't break SQL parsing for quoted template variables#19349daha wants to merge 1 commit into
daha wants to merge 1 commit into
Conversation
Contributor
|
Linear: ING-3336 Thanks for your contribution! We have created an internal ticket to track this PR. A member of the core DataHub team will be assigned to review it within the next few business days - you will get a follow-up comment once a reviewer is assigned. |
daha
force-pushed
the
fix/grafana-quoted-template-variables
branch
from
August 20, 2026 10:13
89e4d7a to
1a41a91
Compare
…ariables
_clean_grafana_template_variables() substituted the quoted literal 'grafana_var'
for ${var} without checking whether the variable was already inside quotes, so
'${var}' became ''grafana_var'' and the query stopped parsing. The ParseError is
swallowed by create_lineage_sql_parsed_result, which returns a truthy result with
no in_tables, so lineage silently fell back to an upstream URN named after the
Grafana datasource UID - a dataset that does not exist in the source platform.
Give the braced pattern the same negative lookbehind/lookahead that the sibling
_GRAFANA_SIMPLE_VAR_PATTERN already carries, so an already-quoted variable is
left alone in both syntaxes.
Those lookarounds test for an adjacent quote rather than string-literal
containment, so a variable in the middle of a longer literal ('%${var}%') is
still substituted and still fails to parse. That case is byte-identical before
and after this change and is shared with the sibling pattern, so the docstring
states what the guard actually does instead of promising more than it delivers.
Tests: a quoted braced variable is preserved; a query mixing quoted and unquoted
braced variables pins the fix as selective rather than as a blanket stop on
braced substitution; and extract_panel_lineage emits the real table URN rather
than the datasource UID. That last query pairs the quoted variable with a
$__timeFilter macro, so cleaning has to run for it to resolve at all and the
test cannot go vacuous. All three fail without the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daha
force-pushed
the
fix/grafana-quoted-template-variables
branch
from
August 20, 2026 15:39
1a41a91 to
d2d8de4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #19346
Problem
_clean_grafana_template_variables()replaces${var}with the quoted literal'grafana_var'without checking whether the variable is already inside quotes. Panel SQL containing
'${var}'—the common way to interpolate a Grafana variable into a SQL string or cast — becomes
''grafana_var'', which no dialect can parse:The irony is that the unmodified query is fine — sqlglot treats
'${var}'as an ordinary stringliteral. The cleaning step is what breaks it.
The failure is then invisible.
create_lineage_sql_parsed_resultswallows theParseErrorandreturns a truthy
SqlParsingResultwithin_tables=[], so_parse_sqlreturns non-None,_create_column_lineagebails at itsif not parsed_sql.in_tablesguard, andextract_panel_lineagefalls through to_create_basic_lineage— which builds the upstream URNfrom the Grafana datasource UID:
That dataset does not exist in the upstream platform, so ingestion materialises a phantom entity
that looks like genuine lineage. Nothing warns —
panel_parsing_warningsstays at 0 andwarningsstays empty.Measured on a production Grafana instance with ~800 dashboards: 143 of 940
upstreamLineageaspects (15.2%) pointed at a fabricated UUID-named dataset; 101 of those are caused by this
issue.
Fix
The sibling
_GRAFANA_SIMPLE_VAR_PATTERNalready carries(?<!')...(?!')for exactly thisreason, and the function's docstring documents that
'$var'is left unchanged. This gives thebraced pattern the same guard, so the documented contract holds for both syntaxes:
Both lookarounds must hold for a substitution, so
'${var}'is left untouched — which is correct,because it is already a valid SQL string literal. Everything else in the diff is the matching
docstring and comment update.
The ordering of the other passes makes this sufficient on its own:
_GRAFANA_GENERIC_MACRO_PATTERN(\$__\w+) runs earlier but cannot match${__from}, because$is followed by{rather than_; and_GRAFANA_SIMPLE_VAR_PATTERNruns later but cannotmatch
'${run_id}'for the same reason. A quoted braced variable now survives all passesunchanged.
Known limitation, shared with the existing sibling pattern and deliberately not addressed here:
a single-quote lookaround is a proxy for "inside a string literal", and it only fires when a quote
is immediately adjacent to the variable. A variable in the middle of a longer literal is still
substituted and still breaks parsing:
That last case produces byte-identical output before and after this change, so it is a pre-existing
gap rather than a regression, and it is exactly how
_GRAFANA_SIMPLE_VAR_PATTERNalready behaves.Closing it properly needs string-literal-aware scanning rather than a wider regex, which is a
larger change than this fix warrants.
Testing
Three tests added, each confirmed failing before the change and passing after.
test_removes_all_grafana_variable_formats[braced_variable_in_quotes]— one new case in theexisting parametrized list, mirroring the
variable_in_quotescase that already asserts thiscontract for
'$var'. A second case,[braced_variables_quoted_and_unquoted], puts a quoted and an unquoted braced variable in onequery, pinning the fix as selective rather than "stopped substituting braced variables".
test_cleaned_query_remains_parseable_with_quoted_variables— feeds the cleaned query backthrough sqlglot and asserts the upstream table is still extractable. The existing suite only
asserted string equality on the cleaner's output, never that the output parses, which is why an
11-case suite passed throughout.
test_extract_panel_lineage_with_quoted_template_variable— end-to-end, asserts the emittedupstream URN is the real table and not the datasource UID. This is the user-visible regression;
the existing
test_extract_panel_lineage_postgresasserts onlylen(upstreams) == 1, whichpasses identically on both the correct and the fabricated-URN path.
Before:
After:
No existing assertion was changed.
./gradlew :metadata-ingestion:lintis clean (ruff check, ruffformat, mypy).
Follow-up, not in this PR
Four further defects in the same function, each producing the same silent fallback, are
deliberately left out — each is independently arguable and would stall review of a one-line fix.
They are described in the linked issue:
$__timeFrom()/$__timeTo()/$__timeGroup(...)are value-producing macros replaced withTRUE. An existing test asserts the current behaviour, so fixing it means changing thatexpectation.
[^)]*, which is not nesting-aware._GRAFANA_SIMPLE_VAR_PATTERNtreats$as a sigil mid-identifier, mangling Oracle identifierssuch as
SOME$COL._parse_sqldiscardsparsed_sql.debug_info.table_error. Surfacing it as a source warning wouldmake this whole class of failure discoverable rather than silent. Note also that
GrafanaSourceReport.report_sql_parsing_{attempt,success,failure}are called only fromfield_utils.pyand never fromlineage.py, sosql_parsing_failuresis structurally always 0for the lineage path, while
report_lineage_extracted()counts the fabricated URN as a success.'%${var}%'— a very common GrafanaLIKEidiom — isstill substituted and still unparseable. Closing it properly means masking string literals before
substituting, which would let both this pattern and its sibling drop their lookarounds entirely.
tests/integration/grafana/contain no Grafana template variables atall, so the whole cleaning path has no end-to-end coverage.
Whether
_create_basic_lineageshould emit a made-up dataset name at all is a design question formaintainers, and is also left alone here.
Checklist
🤖 Generated with Claude Code
Summary by cubic
Preserves quoted braced Grafana variables during cleaning so SQL keeps parsing. Previously
'${var}'became''grafana_var'', parsing failed, and lineage fell back to a datasource-UID dataset; now quoted${...}is left as-is so upstreams resolve to the real table.(?<!')\$\{[^}]+\}(?!')and align docs/comments; pass ordering continues to substitute unquoted variables.sqlglot, and an end-to-end lineage test that asserts the real table URN with a realSchemaResolver.%${var}%can still be substituted and fail to parse. No config or migration changes.Written for commit d2d8de4. Summary will update on new commits.