Skip to content

fix(dbt): substitute DERIVED metric references in a single pass - #353

Open
ayushtkn wants to merge 2 commits into
apache:mainfrom
ayushtkn:fix/dbt-derived-metric-substitution
Open

fix(dbt): substitute DERIVED metric references in a single pass#353
ayushtkn wants to merge 2 commits into
apache:mainfrom
ayushtkn:fix/dbt-derived-metric-substitution

Conversation

@ayushtkn

@ayushtkn ayushtkn commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

MSIToOssieConverter._resolve_derived inlined each input metric of a DERIVED metric
with its own re.sub call, run over the string produced by the previous iteration.
Because every pass re-scanned text that earlier passes had inserted, and because the
resolved expression was passed as re.sub's replacement template, the emitted Ossie
expression could be silently corrupted in two ways.

1. A later reference matched text an earlier one inserted. MetricFlow metrics are
commonly named after the column they aggregate, so this is easy to hit. With measures
gross = SUM(net) and net = SUM(net_amount) and a DERIVED metric expr = "gross - net":

before:  SUM(orders.SUM(orders.net_amount)) - SUM(orders.net_amount)
after:   SUM(orders.net) - SUM(orders.net_amount)

2. Backslashes in the resolved SQL were read as replacement escapes. A backslash
surviving from a metric filter was reinterpreted on inlining. With a SIMPLE metric
filtered on {{ Dimension('order__path') }} LIKE 'a\b', inlined into expr = "revenue * 2":

before:  SUM(CASE WHEN order__path LIKE 'a<backspace>' THEN orders.amount END) * 2
after:   SUM(CASE WHEN order__path LIKE 'a\b' THEN orders.amount END) * 2

The \b became a literal backspace character. Other sequences (e.g. \d) raised
re.error: bad escape and aborted the conversion instead.

The change. Collect the {reference: resolved expression} pairs first, then
substitute them in a single pass using an alternation pattern with a callback
replacement. A callback is not template-expanded, so one change fixes both problems.
This is the same approach already used in the Databricks converter
(metric_view_to_ossie.py, re.sub(r"\bsource\.", lambda _m: ...)).

Related Issues

NA

Checklist

Specification

  • Spec changes are included in core-spec/ and follow the existing structure
  • Spec changes have been discussed on the mailing list or in a linked issue
  • Breaking changes to the spec are clearly called out in the summary

Ontology

  • Ontology changes in ontology/ are consistent with spec changes
  • New or modified terms are defined and documented

Converters

  • Converter logic in converters/ is updated to reflect spec or ontology changes
  • New converters include tests under the converter's test directory

Validation

  • Validation rules in validation/ are updated if the spec changed
  • New validation cases are covered by tests

Documentation

  • docs/ is updated to reflect any user-facing changes
  • New features or behaviors are documented with examples where appropriate
  • CONTRIBUTING.md is updated if the contribution process changed

Examples

  • examples/ are added or updated for any new spec constructs or converter support

Tests

  • All existing tests pass (pytest / CI green)
  • New functionality is covered by tests

Compliance

  • ASF license headers are present on all new source files
  • No third-party dependencies are added without PMC/IPMC approval

_resolve_derived ran one re.sub per input metric over the string produced
by the previous iteration, so each pass re-scanned text that earlier
passes had inserted. A metric named after a column appearing in an
already-inlined expression was expanded twice, e.g. `gross - net` with
gross = SUM(orders.net) yielded SUM(orders.SUM(orders.net_amount)).

Passing the resolved expression as re.sub's replacement also let it be
read as a template: a backslash surviving from a metric filter was
reinterpreted, turning `LIKE 'a\b'` into a literal backspace character
(and raising re.error for sequences such as \d).

Collect the references first and substitute them in one pass with a
callback replacement, which is not template-expanded, fixing both.
Copilot AI lite review requested due to automatic review settings September 2, 2026 06:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The refactor directly addresses the described substitution correctness issues and is covered by targeted regression tests, with only minor non-blocking maintainability feedback.

Pull request overview

This PR fixes DERIVED metric reference inlining in the dbt MSI→Ossie converter by performing all substitutions in a single regex pass with a callback replacement, preventing both accidental re-expansion of already-inlined text and re.sub replacement-escape corruption (e.g., backslashes from filters).

Changes:

  • Refactors MSIToOssieConverter._resolve_derived to collect reference→resolved-expression mappings and substitute them in one re.sub pass via a compiled alternation pattern and callback.
  • Adds regression tests covering (1) no re-expansion when one metric name appears inside another metric’s resolved SQL and (2) preservation of backslashes originating from filters.
File summaries
File Description
converters/dbt/src/ossie_dbt/msi_to_ossie.py Changes DERIVED metric substitution to a single-pass alternation+callback approach to avoid re-expansion and replacement-escape issues.
converters/dbt/tests/test_msi_to_ossie.py Adds tests validating single-pass substitution behavior and correct backslash preservation from filtered metrics.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +364 to +367
# longer identifier; sorting by length keeps the alternation order stable
# and independent of the order metrics happen to be declared in.
pattern = re.compile(
r"\b(" + "|".join(re.escape(ref) for ref in sorted(replacements, key=len, reverse=True)) + r")\b"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

sorted(replacements, key=len, reverse=True) is only deterministic for
references of differing lengths; ties keep insertion order, which is the
order the input metrics are declared in. Sort by length and then by name
so the compiled pattern is fully stable.

Matching is unaffected: the \b anchors and the equal length mean at most
one tied alternative can match at a given position either way.
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.

2 participants