Add PowerBI <> Ossie Converters - #329
Conversation
…-valid
The initial `converters/microsoft` drop could not be imported, installed or
validated. This fixes the three blocking issues and adds the tests that catch
them:
1. Broken public API. `__init__.py` re-exported `semantic_model_to_ossie`
while the module defines `convert_semantic_model_to_ossie`, so importing
the package raised ImportError.
2. Not a package. The sources sat directly in `src/` and both
`pyproject.toml` and `README.md` were empty, so the converter could not
be installed, run or tested. Sources now live in `src/ossie_microsoft/`
(matching the other Python converters), with a populated pyproject, an
`ossie-microsoft` CLI, a README documenting the mapping and limitations,
ASF license headers and a CI workflow.
3. Output did not validate. The converter emits a `DAX` dialect that was not
part of the spec, so every generated document failed schema validation:
[Schema] ... -> dialect: 'DAX' is not one of ['ANSI_SQL', 'SNOWFLAKE',
'MDX', 'TABLEAU', 'DATABRICKS', 'MAQL', 'BIGQUERY']
`DAX` is added to the Dialect enum in osi-schema.json, spec.yaml, spec.md
and OSIDialect, and to validate.py's skip list because sqlglot cannot parse
DAX. `POWER_BI` is registered as a vendor name for custom extensions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The TMSL -> Apache Ossie importer previously dropped everything that had no Apache Ossie counterpart, and did so silently. Rework it around a new shared module so a conversion is auditable and, where possible, reversible: * Add `ossie_microsoft/_common.py` with the constants, the bidirectional data type mapping and the `custom_extensions` stash protocol shared by both conversion directions, mirroring `converters/databricks`. * Route every lossy step through `warn()` so callers can escalate warnings to errors and get a hard guarantee that a conversion was lossless. * Preserve unrepresentable TMSL properties (annotations, partitions, hierarchies, roles, perspectives, cultures, format strings, display folders, KPIs, cross-filter behaviour) plus excluded tables and skipped relationships in a versioned POWER_BI `custom_extensions` blob. * Correct the data type mapping. TMSL has no date-only, time-only or timezone-aware member, so the previous 'date'/'time' entries could never fire; date-only intent is now detected from the format string instead. `double` maps to Float rather than Decimal, `binary`/`variant` map to Opaque with a warning, and `automatic`/`unknown` omit the type instead of inventing one. * Record a measure's home table and a flipped relationship's original orientation so the export direction can rebuild the model faithfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The converter was import-only. Add `ossie_to_semantic_model` so an Apache Ossie model can be materialized as a Power BI `model.bim`, completing the round trip, and expose it through the package and an `export` subcommand. Expressions are never rewritten between languages. A metric is emitted as a measure only when it carries a DAX expression; a field becomes a `sourceColumn` only when its expression is a plain column reference. A SQL aggregate mechanically rewritten into DAX ignores filter context, so it would yield a measure that is wrong rather than one that is missing -- those cases warn and are skipped instead. Constructs Power BI cannot express are reported rather than approximated: composite keys and composite relationships have no equivalent, `Opaque` has no data type, and `Time`/`DateTimeTz` collapse onto `dateTime`. A dataset with no preserved partition gets a placeholder that raises an M `error` naming the missing source, so a refresh fails with an actionable message instead of loading nothing. A `model.bim` converted to Apache Ossie and back reproduces the original model, including the tables, relationships and properties the import left out of the vendor-neutral document. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the suite from 26 to 82 tests, adding the export direction and the behaviour introduced with it: * A round-trip test asserting a `model.bim` converted to Apache Ossie and back is structurally the same model, and a second test asserting a further pass is a fixed point, so the pipeline cannot drift. * Explicit tests for each construct the converter refuses to guess at: computed SQL expressions, metrics with no DAX, composite keys, composite relationships and relationships with a missing endpoint. * A test that escalating warnings to errors passes for a model with no Power BI specifics, which is the guarantee callers rely on to prove a conversion was lossless. * Full data type coverage, including date-only detection from a format string and the quoted-literal case where 'h' is display text rather than an hour token. The fixture gains column and table annotations, a format string, a display folder and a summarizeBy so the passthrough paths are exercised on real data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The README described an import-only converter. Rewrite it around what the package now does and, more importantly, why it refuses to do certain things: * State the design rule -- expressions are carried across in the dialect they were authored in and never machine-translated -- and explain the failure mode it avoids, namely a model that loads and returns the wrong number rather than one that visibly lacks a measure. * Document the losslessness contract: what is preserved in the POWER_BI stash, and how to escalate warnings to errors to prove a conversion lost nothing. * Add the data type table with the caveats that actually bite -- TMSL has no date-only, time-only or timezone-aware type, date-only intent travels in the format string, 'm' means month in a VBA-style format unless it follows an hour token, and 'double' is approximate so it maps to Float not Decimal. * Tabulate the constructs that have no equivalent in either direction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A named format such as `Short Date` is a whole-string name, not a sequence of tokens, so tokenizing it misreads the `h` in "Short" as an hour token and the `n` in "Long" as a minute token. Both were classified as having a time part, so a date-only column round-tripped as DateTime. Match the named formats before tokenizing, and cover them plus the numeric formats that must not be mistaken for dates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two ways the losslessness guarantee leaked, both found in review: * The stash used per-level allow-lists, so any TMSL property not named in them was dropped without a warning -- keepUniqueRows, alignment, displayOrdinal and sourceProviderType among them, and any property a future TMSL version adds. Invert them into deny-lists keyed on what the Apache Ossie mapping actually consumes, so an unrecognized property is preserved by default rather than lost by default. * Relationship cardinalities were recorded only when the import had to flip a one-to-many relationship. A one-to-one relationship therefore exported without cardinalities and picked up the TMSL many-to-one defaults, silently widening it. Record cardinalities whenever the source states them. Also preserve the two things the import legitimately sets aside: rowNumber columns, and a dataType with no portable equivalent (binary, variant, automatic, unknown) so the export restores the original TMSL type instead of guessing one back from the portable model. The test fixture now round-trips structurally identical through bim -> Apache Ossie -> bim. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two more ways the stash could produce a wrong model, both found in review: * The replay loops assigned stashed values over properties the mapping had already derived. Importing a `type: data` column, editing its Ossie expression to DAX and exporting produced a column carrying both `type: data` and an `expression`, which is contradictory TMSL. Replay with setdefault throughout, so the core document is the source of truth whenever the two disagree and preserved values only fill gaps. * TMSL allows a description on both the document and the model, but Apache Ossie has one. A document-level description was moved onto the model, and when both were present the document one was dropped. Record which one the Apache Ossie description came from and keep the other verbatim, so both return to where they were authored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Its test suite validates converter output against core-spec/osi-schema.json, so a schema change can break it without touching converters/microsoft. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A measure's result type is inferred by the engine from its DAX expression; TMSL has no writable dataType on a measure, so emitting one risks Analysis Services rejecting the model. The export derived it from the Apache Ossie metric datatype, and the test fixture carried the property, which made the round-trip tests treat it as legitimate. The export now warns instead of emitting, and the import preserves a dataType verbatim in the stash if a source model happens to carry one, rather than reconstructing it from the portable type. The measure keeps its Apache Ossie datatype for consumers that want the hint; it is simply never pushed back into TMSL. The fixture drops the property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The export skipped the data type mapping entirely whenever the stash held a TMSL type, so a preserved `binary`, `variant`, `automatic` or `unknown` was replayed even after someone had edited the Apache Ossie datatype to something else. That is the one remaining place the stash outranked the core document. Replay the stashed type only while the portable type still agrees with it, which is exactly the condition under which it was stashed in the first place. Once the core datatype changes, it is authoritative. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A TMSL sourceColumn names a column in the table's source query, which may be spelled with spaces, a hyphen or a leading digit -- "Order Date" is ordinary in a Power BI model. The export only accepted a bare SQL identifier, so any such column was treated as a computed expression and dropped, taking a valid column out of the model on a plain round trip. Preserve a sourceColumn that is not a bare identifier and replay it while the field expression still matches it. An edited expression continues to take precedence, consistent with the rest of the stash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Power BI stash carries a format version, but it was discarded without being read. A payload written by a later converter would have been replayed under this converter's assumptions, which is precisely the silent-wrong-answer outcome the design rule exists to prevent. Reject a version this converter does not understand, and a non-integer version, with a message that says which version was found and what to do. An absent version is still read as the current format, so documents written before this change keep working. Also record an Analysis Services deployment smoke test in the roadmap: these tests check the shape of the TMSL, not that the engine accepts it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round-trip losslessness was already guaranteed by the Power BI stash, and that guarantee hid something: an Apache Ossie document is meant to be read by tools that are not Power BI, and to those tools a preserved-but-unmapped construct is simply absent. Row-level security roles, perspectives, translations, hierarchies, KPIs, calculation groups, sort-by-columns and date table variations all crossed over in silence. Report them, at the level where they occur, saying which model has no counterpart and what happened to it. The import says "preserved for round trip but not represented"; the export says "dropped", because a TMSL document has nowhere to keep an Ossie construct -- ai_context and label were being discarded with no signal at all. Purely presentational properties (isHidden, displayFolder, formatString) stay unreported on purpose: a warning on every cosmetic property would bury the ones that matter. Each report now goes to both a UserWarning and the ossie_microsoft logger. The two answer different questions -- a warning filter gives a caller a hard programmatic guarantee of losslessness, while a log serves an application that never installs one. warn() also gained stacklevel=2 so a -W error traceback names the conversion call site instead of the helper. The CLI grows --strict, which exits non-zero if anything could not be carried across faithfully, and -q/--quiet. Both are accepted before or after the subcommand, because that is where people type them; argparse needed SUPPRESS defaults on the subcommand copies to stop them overwriting an already-parsed value. The CLI silences the warning channel so each message prints once. The fixture gains all of these constructs, which proves both halves at once: every one is reported, and the model still round-trips structurally identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The converter's contract is that every lossy branch reports itself, which is only worth as much as the tests that exercise it. Nothing enforced either the lint rules the rest of the repo uses or the coverage that makes the contract credible. Adopt ruff with the rule set converters/gooddata and converters/orionbelt already use, and enforce branch coverage at 95% (currently 97%). The bar is deliberately high: an unexercised branch is an unproven warning. CI now lints, measures coverage, round-trips the fixture through the installed console script -- which is the only step that proves the entry point is wired up -- and validates the result against the core spec. pytest's default warning filter is set to ignore, so the suite runs clean with a bare `pytest`. This does not weaken anything: the tests that assert a lossless conversion is silent turn warnings into errors themselves, locally and explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ossie into m-kovalsky/sm_to_osi
The converter had been flattened from src/ossie_microsoft/*.py to loose
modules directly under src/. That is off-convention here (all nine Python
converters use src/<package>/) and it broke the distribution in ways the
unit tests could not see, because they import through pythonpath = ["src"]
and so pass under either layout:
* the wheel shipped _common.py, cli.py, ossie_to_semantic_model.py and
semantic_model_to_ossie.py as top-level modules, squatting on generic
names in every environment that installed it, while omitting
__init__.py entirely, so `import ossie_microsoft` failed after install;
* the console script pointed at "cli:main", which no longer resolved;
* coverage measured flat module names that no longer existed.
Move the modules back under src/ossie_microsoft/, restore relative sibling
imports so the package does not depend on src/ being on sys.path, and
point the console script, wheel target and coverage source at the package.
Add tests/test_packaging.py to keep this from regressing silently. Beyond
checking the declarations, it builds the wheel and inspects it, since the
declarations can be correct while the built artifact is not. All fourteen
of its assertions fail on the flattened layout.
Also credit the individual contributors via [project] maintainers, leaving
authors as the ASF to match the sibling converters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # converters/README.md
The importer had been rewritten in a way that dropped the vendor stash
entirely: it no longer called write_stash, so every TMSL construct without
an Apache Ossie counterpart was silently discarded instead of preserved.
That contradicted the README, the exporter and 54 tests. The exporter was
never changed to match and still reads the stash at six sites, so the two
directions had simply come apart.
Restore the stash-aware importer and carry the newer work forward onto it
rather than the other way around, since the newer file was a strict subset:
* calculated tables are excluded from the model, and the reason a table
was excluded is now reported specifically ("calculated tables are not
converted to Apache Ossie") instead of listing every rule that might
have applied;
* relationship cardinalities are compared case-insensitively.
Roles, perspectives, cultures, shared expressions, query groups, excluded
tables and per-column, per-measure and per-relationship properties are
preserved again, and sales_model.bim now round trips with all six tables
and every model-level construct intact.
Two problems found while restoring this:
Expression annotations were written unconditionally, including for DAX.
DAX goes straight into the TMSL expression property, so the annotation
only duplicated it and made a model that had merely round tripped differ
from the original. Annotate only when the authored dialect is not DAX,
which is the case where the text would otherwise be lost.
A delimited table reference such as "my schema"."my table" was rejected by
the bare-identifier rule and fell through to the query branch, which
emitted Sql.Database(..., [Query="""my schema"".""my table"""]) - not
valid SQL, and exactly the kind of plausible-looking expression the
converter is not supposed to invent. Hold only undelimited parts to the
identifier rule.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Power BI evaluates a measure or calculated column only as DAX; TMSL has no property that can carry an expression in another dialect. The exporter handled that by writing BLANK() into `expression` and parking the authored text in OssieExpression / OssieExpressionDialect annotations. That produces a model which deploys and refreshes without error and then answers every query involving the object with a wrong number. Nothing in the Power BI experience surfaces the annotations, so the failure is silent. An absent measure, by contrast, is something a modeller notices immediately. A non-DAX expression is now reported through `warn` and the object is skipped, matching how the converter already handles every other construct it cannot represent. The authored expression is untouched in the Apache Ossie source, so re-running the conversion after adding a DAX expression picks it up. The annotation pair existed only to accompany the stand-in and nothing read it back, so it is removed along with it. `OssieAIContext` is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Power BI evaluates a measure only as DAX, so a metric authored in SQL was
skipped outright. That is safe but unhelpful: the most common metrics in an
Apache Ossie model are a single aggregate over one column, and those have an
unambiguous DAX equivalent.
Adds a translator covering exactly that tier -- SUM/MIN/MAX/COUNT/AVG/MEDIAN,
the sample and population STDDEV/VARIANCE forms, COUNT(DISTINCT x) and
COUNT(*) -- reading ANSI_SQL, Snowflake, Databricks and BigQuery via sqlglot,
which is already a runtime dependency of the dbt and gsf converters.
The mapping is not a passthrough: DAX renames AVG to AVERAGE, STDDEV to
STDEV.S, VARIANCE to VAR.S and COUNT(DISTINCT x) to DISTINCTCOUNT, so emitting
the SQL spelling unchanged would be silently wrong. The mappings follow the
table in core-spec/expression_language.md.
DAX also has no bare column reference, so a metric only translates when its
column resolves to exactly one field in exactly one dataset. The SQL names the
physical column, which TMSL carries as sourceColumn, while DAX addresses the
column by its model name -- so the translation maps between the two. A name
found in two datasets is refused rather than guessed at.
Everything outside the curated set is reported and skipped as before:
arithmetic between aggregates, aggregates over expressions, CASE, windowed and
filtered aggregates, qualified references, and percentiles, whose DAX spelling
depends on an interpolation the SQL does not state. Calculated fields are still
never translated, because they evaluate in row context where SUM('T'[X]) would
return the whole-column total on every row.
Tests weight refusal over breadth, since a missed translation is a nuisance
while a wrong one deploys cleanly and reports bad numbers. Every emitted form,
including bracket and quote escaping, was checked to parse as valid DAX.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| # preserved verbatim in the stash. This is deliberately a deny-list rather than an | ||
| # allow-list: TMSL grows new properties over time, and an allow-list would silently drop | ||
| # any it had not been taught about, which is exactly what the losslessness rule forbids. | ||
| _MODEL_CONSUMED = frozenset({"description", "tables", "relationships"}) |
There was a problem hiding this comment.
TMSL annotations are not in _TABLE_CONSUMED/_MODEL_CONSUMED, so they're never mapped back to ai_context on import.
If a dataset's ai_context was exported to an OssieAIContext annotation, re-importing that TMSL sweeps the annotation into custom_extensions instead of restoring ai_context.
There was a problem hiding this comment.
Addressed in f015ef2. Import now extracts only the OssieAIContext annotation into ai_context at model, dataset, field, metric, and relationship scope; structured JSON is decoded while scalar text remains text, and every unrelated annotation stays in the Power BI stash for replay. Regression coverage exercises all five scopes, string/object forms, and preservation through a full round trip. Validated in the integrated stack: 383 tests, 95.13% branch coverage, Ruff clean, TOM round-trip clean, and live Power BI engine validation clean.
| tables.extend(stash.get("excludedTables") or []) | ||
|
|
||
| model = {"tables": tables} | ||
| if generated_partitions: |
There was a problem hiding this comment.
model["expressions"] is assigned unconditionnally here whenever any table needs a generated Direct Lake partition, but this runs before the stash-replay loop model.setDefault(...) below, so a preserved model-level expression from a prior import can never win.
If the document mixes a stashed table with one brand-new dataset needing a generated partition, this silently overwrites the shared DatabaseQuery M expression for every table, not just the new one.
There was a problem hiding this comment.
Addressed in 2bf9bf4. Generated Direct Lake partitions are tracked individually; preserved model expressions are merged rather than replaced, compatible DatabaseQuery expressions are reused, and an explicit conflicting source gets a deterministic collision-free name wired only to newly generated partitions. Tests cover mixed preserved/new tables, unrelated expressions, compatible reuse, name collisions, and unchanged old partition references. Integrated validation: 383 tests, 95.13% coverage, TOM and live Power BI round trips clean.
| bim = {"name": semantic_model.get("name") or "semantic_model"} | ||
| if description and stash.get("descriptionSource") == "document": | ||
| bim["description"] = description | ||
| bim.update(document_properties) |
There was a problem hiding this comment.
bim.update(document_properties) can restore a stashed compatibilityLevel before this setdefault runs, so setdefault silently no-ops even when the current export needs a different level.
Should we do this check before honoring the stashed value, or override it when generated_partitions is true?
There was a problem hiding this comment.
Addressed in f31b9fd. Compatibility is now treated as a feature minimum: exports with newly generated Direct Lake partitions use max(stashed, 1702), higher valid levels are retained, malformed levels fail explicitly, and import/generated-M partitions do not force a Direct Lake upgrade. Low/equal/high/no-Direct-Lake cases are covered. The exported round trip also passed TOM structural validation and a real Fabric deploy/refresh/evaluate/delete cycle.
| to_table, to_column): | ||
| continue | ||
|
|
||
| if stash.get("flipped"): |
There was a problem hiding this comment.
This unconditionally swaps from/to back to the original orientation whenever stash.get("flipped") is true, with no staleness check.
If a user hand-edits the YAML to intentionally reverse a relationship, this silently reverts their edit back to the pre-edit orientation on export.
There was a problem hiding this comment.
Addressed in 3bf699c. Import records the normalized endpoint snapshot with orientation/cardinality metadata. Export restores the original Power BI orientation only while the current Ossie endpoints still match that snapshot; edited endpoints win, and stale cardinalities are not replayed. Legacy stashes retain a conservative generated-name freshness check. Tests cover exact unchanged round trip, reversed endpoints, edited columns, stale cardinalities, and legacy payloads.
| continue | ||
| scope = f"table '{table['name']}' measure '{measure.get('name')}'" | ||
| warn_unsupported(scope, measure, TMSL_UNSUPPORTED_MEASURE, "Apache Ossie", _PRESERVED) | ||
| expression = text(measure.get("expression", "")).strip() |
There was a problem hiding this comment.
Every other exclusion case in this module (private tables, calculation groups, calculated tables, auto-date tables, unresolvable relationships) gets stashed for round-trip preservation, but a blank-expression measure is just warned about and dropped here with no equivalent excludedMeasures-style stash.
That makes it permanently unrecoverable rather than round-trippable.
Is it intentional, or should we preserve it like the other exclusions?
There was a problem hiding this comment.
Addressed in a9c45ed. Expressionless measures are now preserved verbatim in version-2 excludedMeasures model stash entries with home table and original position, then restored on export. Current authored metrics are applied first and win on table/name collisions; missing home tables and malformed preserved entries are reported rather than silently guessed. Tests cover exact payload/order round trip, multiple tables, missing homes, malformed entries, and authored-metric replacement.
| expressions = dialect_expressions(field.get("expression")) | ||
| if expressions: | ||
| _, expression = _preferred_expression(expressions) | ||
| candidate = expression.strip('"').strip("`").strip("[]") |
There was a problem hiding this comment.
_dataset_column_index only registers unqualified alias keys, but _column_index (used for metrics) also registers a table.column-qualified key for every column. Since both feed the same _sql_to_dax resolver, a calculated column that self-qualifies its own column (let's say customer.first_name where the dataset is named customer) fails to resolve here even though the identical style works for metrics.
There was a problem hiding this comment.
Addressed in 58ceafa. _dataset_column_index now registers both unqualified and case-insensitive dataset.alias keys for field names and physical/source aliases while retaining the existing uniqueness filter. Tests cover qualified model names, qualified source aliases, case variation, and ambiguous/foreign/over-qualified references falling back to the explicit BLANK() refusal path.
| with urllib.request.urlopen(request) as response: # noqa: S310 | ||
| body = response.read().decode("utf-8") | ||
| return response.status, (json.loads(body) if body else None), dict(response.headers) | ||
| except urllib.error.HTTPError as exc: |
There was a problem hiding this comment.
_request only catches HTTPError, not URLError (DNS failure, reset, timeout). Since deploy() is called outside the try/finally that guards the workspace-item DELETE cleanup, a transient URLError during operation polling propagates straight out of validate_with_engine, leaving an already-created Fabric workspace item undeleted even with keep=False.
There was a problem hiding this comment.
Addressed in 2acea83, with integration diagnostics in 5df6212 and coverage in 8813262. _request normalizes URL/DNS/connection/timeout failures; operation and refresh polling retry bounded transient failures; orchestration enters finally with the dataset ID whenever one is available; cleanup failures are surfaced as failures; and an unknown ID explicitly reports that cleanup could not be attempted. Regression tests cover transient recovery, persistent transport failure, known-ID cleanup, cleanup failure, and keep=True. A real Fabric deploy/refresh/evaluate completed successfully and both validation items were confirmed deleted afterward.
| result = _wait_for_operation(operation, token) | ||
| if result.get("status") != "Succeeded": | ||
| return None, json.dumps(result)[:4000] | ||
| _status, created, _headers = _request("GET", f"{operation}/result", token) |
There was a problem hiding this comment.
created["id"] assumes the follow-up GET succeeded, but _request failure branch returns a string, not a dict.
If that GET fails (expired token, transient 5xx) after the create operation itself succeeded, this raises TypeError: string indices must be integers instead of deploy() documented (None, "HTTP ...") return. It crashes validate_with_engine rather than surfacing a clean error.
There was a problem hiding this comment.
Addressed in df70115, reconciled with transport handling in 5df6212 and covered further in 8813262. The async result GET now validates status, body type, and a non-empty string ID before indexing; retries only transient HTTP/transport results; fails immediately for auth errors; and returns (None, diagnostic) for malformed successful responses instead of raising TypeError. Tests cover 500/string, 401/envelope, missing/empty IDs, transient HTTP recovery, and transient transport recovery. Full integrated suite: 383 passed at 95.13% coverage.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jbonofre
left a comment
There was a problem hiding this comment.
It looks good to me now.
I re-triggered the failed job on CI.
We should fix converters/microsoft/uv.lock: we should run uv lock and commit the result, and add --locked to the uv sync step in the CI workflow so a drifted lock fails loudly instead of silently re-resolving.
uv.lock only listed pyyaml, missing sqlglot, the tom extras, and the dev toolchain that pyproject.toml already declares, so uv sync was silently re-resolving dependencies fresh on every run instead of using a pinned lock. Regenerate the lock and pass --locked in CI so future drift fails the build instead of resolving silently.
|
I took the liberty to push the fix about |
|
I'm now investigating the CI failure. |
Adding -l to pytest to capture the full contents of the measures dict when test_all_tpcds_example_metrics_translate_to_dax fails on CI but not locally. Will be reverted once the cause is found.
Local variable output is truncated by pytest's default repr limit, so add an explicit, untruncated print of the measure name set to see exactly what test_all_tpcds_example_metrics_translate_to_dax produced on CI's Linux runner. Will be reverted with the -l flag once resolved.
…DS window-function metrics
main added cumulative_sales, brand_rank_in_store, and monthly_sales_change to the shared TPC-DS example after this branch was cut, each wrapping an aggregate in an OVER clause. This translator correctly refuses window functions rather than guess at their DAX form, so test_all_tpcds_example_metrics_translate_to_dax's blanket "never BLANK()" assertion no longer held once CI merged this branch with current main. Assert the five plain-aggregate metrics still translate exactly as before, and that the three window-function metrics explicitly come back as BLANK().
|
I updated the PR in order to fix the CI. |
Summary
This PR contains converters to and from Microsoft Power BI Semantic Models and Ossie models.
Checklist
Specification
Validation
Examples
examples/are added or updated for any new spec constructs or converter supportTests