Skip to content

feat(microsoft): opt-in DAX to OSSIE_SQL_2026 translation for single-column aggregate measures (#584) - #623

Closed
MrBlack1995 wants to merge 2 commits into
apache:mainfrom
MrBlack1995:feat/microsoft-dax-to-sql-pr1
Closed

MrBlack1995 wants to merge 2 commits into
apache:mainfrom
MrBlack1995:feat/microsoft-dax-to-sql-pr1

Conversation

@MrBlack1995

@MrBlack1995 MrBlack1995 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First slice of apache/ossie-converters#23: an opt-in, deterministic DAX to OSSIE_SQL_2026 translator for Power BI measures on import, so the most common measures leave Power BI in a portable form every Ossie consumer can read.

Today every imported measure is DAX only. With --translate-dax, a measure that is a single aggregate over one fully qualified column gets OSSIE_SQL_2026 as its primary dialect, with the original DAX kept as a fallback, so nothing is lost:

# ossie-microsoft import -i sales_model.bim --translate-dax
- name: Total Sales
  expression:
    dialects:
      - dialect: OSSIE_SQL_2026
        expression: SUM(Sales.Amount)
      - dialect: DAX
        expression: SUM ( Sales[Amount] )

Everything else is refused and kept exactly as today (DAX only), with a warning naming the reason:

[table 'Sales' measure 'Order Count'] DAX measure kept as-is; not translated to OSSIE_SQL_2026 (COUNTROWS counts rows, not a single column; it is a later tier)

Change

  • New ossie_microsoft/_dax_to_sql.py, the reverse of _sql_to_dax.py. It translates exactly the functions _sql_to_dax emits, so an Ossie SQL model exported to Power BI and imported again comes back to the same expression:

    DAX OSSIE_SQL_2026
    SUM('T'[c]) SUM(T.c)
    AVERAGE('T'[c]) AVG(T.c)
    MIN('T'[c]) / MAX('T'[c]) MIN(T.c) / MAX(T.c)
    COUNTA('T'[c]) COUNT(T.c)
    DISTINCTCOUNTNOBLANK('T'[c]) COUNT(DISTINCT T.c)
  • Refused on purpose, following the reasoning already written down in _sql_to_dax: DAX COUNT (TRUE/FALSE values unsupported, so it is not SQL COUNT(x) for an arbitrary column) and DISTINCTCOUNT (counts BLANK as a distinct value). The empty-set convention (DAX BLANK vs SQL 0) is the one _sql_to_dax already accepts.

  • Also refused: anything with more than one argument or a nested call (COUNTROWS, iterators, CALCULATE, DIVIDE, arithmetic, IF, time intelligence), unqualified columns, a column that does not resolve to exactly one dataset field, and names that would need SQL quoting. These are later tiers.

  • translate_dax=False is threaded through convert_semantic_model_to_ossie, build_ossie_document and _convert_metrics; the import subcommand gets --translate-dax. A resolver maps 'Table'[Column] to the Ossie dataset.field built from the model.

Answers to the questions on apache/ossie-converters#23, as implemented (easy to change)

  1. Lives in converters/microsoft, in Python, as a sibling of _sql_to_dax.py.
  2. Opt-in, default off. Default output is unchanged, so existing Power BI round trips are unaffected. If you would rather have it on by default, that is a one-line change.
  3. No catalog format yet: this slice is a six-entry mapping. The Python code vs catalog.yaml question can be settled in the PR that grows it.

Measured effect

Running the portability fixtures in #596 (M0 to M3) through Ossie to Power BI to Ossie:

DAX only OSSIE_SQL_2026 BLANK() placeholder
today (flag off) 16 0 2
--translate-dax 0 16 2

The 16 returned expressions are identical to the original SQL (SUM(sales.amount) comes back as SUM(sales.amount), COUNT(DISTINCT sales.customer_id) as COUNT(DISTINCT sales.customer_id), and so on). The 2 placeholders are the M3 filtered aggregates, which the SQL to DAX export already reduces to BLANK(); that is the export side and out of scope for an import translator.

Validation

  • converters/microsoft: 468 passed, 2 skipped locally; ruff check clean. 28 new tests in tests/test_dax_to_sql.py (golden translations, every refusal, reasons, quoted names, and integration tests for flag off, a translated measure with DAX fallback, and a refused measure).
  • On tests/fixtures/sales_model.bim: flag off output is unchanged; flag on output passes validation/validate.py; exporting the flag-on document back to Power BI gives the original DAX for every measure.
  • No existing behavior changes unless the flag is passed.

Related Issues

Refs apache/ossie-converters#23. Measured against the fixtures in #596 (#576).

Checklist

Converters

  • Converter logic in converters/ is updated (Microsoft import, opt-in)
  • Tests added; existing suite passes

MrBlack1995 and others added 2 commits October 21, 2026 06:21
The reverse of _sql_to_dax: a deliberately narrow, deterministic translator for
single-column aggregate measures (SUM, AVG, MIN, MAX, COUNT via COUNTA,
COUNT DISTINCT via DISTINCTCOUNTNOBLANK), refusing everything else with a
reason. Translates exactly the functions _sql_to_dax emits, so an Ossie SQL
model exported to Power BI and re-imported round-trips to the same expression.

COUNT and DISTINCTCOUNT are refused on purpose: DAX COUNT does not support
TRUE/FALSE values and DISTINCTCOUNT counts BLANK as a distinct value, so neither
is the exact equivalent of SQL COUNT(x) / COUNT(DISTINCT x); translating them
could be a silent wrong number on another engine. Refuse-by-default is the house rule from _sql_to_dax and the
argument in apache#437.

This is the translator core only; wiring it into _convert_metrics behind an
opt-in --translate-dax flag (default off) is the next commit. No existing file
is touched, so default behavior is unchanged. 24 unit tests pass.

Refs #584

Co-authored-by: Isaac <no-reply@databricks.com>
…(default off)

Threads translate_dax (default False) through convert_semantic_model_to_ossie,
build_ossie_document and _convert_metrics. When on, a single-column aggregate
measure is translated to OSSIE_SQL_2026 as the primary dialect with the original
DAX kept as a fallback, so nothing is lost; a measure that cannot be translated
safely is kept as DAX with a diagnostic naming the reason. A resolver maps a DAX
Table[Column] to the Ossie dataset.field built from the model.

Default output is byte-for-byte unchanged (flag off keeps every measure as DAX).
Adds the --translate-dax flag to the import subcommand. Integration tests cover
default-off, a translated aggregate with DAX fallback, and COUNTROWS refused;
_dax_to_sql added to the packaging manifest. Full suite: 468 passed, 2 skipped.
Translated output passes validation/validate.py.

Refs #584

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

Copy link
Copy Markdown
Member

@MrBlack1995 as shared on the dev mailing list, the converters have moved to a dedicated repository: https://github.com/apache/ossie-converters

Can you please create this PR on the new repo? Thanks!

@MrBlack1995

Copy link
Copy Markdown
Contributor Author

Thanks @jbonofre, re-opened as apache/ossie-converters#6.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants