Guide · Power BI and Tableau Migration

Which DAX measures a converter can't translate: 271 measures checked

DuckDB2026-10-18Script ran before publishing

The short answer

In 12 Microsoft sample reports (271 measures), searching for the function names Snowflake lists finds 38 (14.0%). Adding 13 that read calculation groups or calculated tables, which Snowflake says are dropped, and 88 that build on a flagged measure, our assumption, gives 139 (51.3%). We did not run a converter.

What you will have at the end

One Python file, dax_converter_gaps.py, that downloads Microsoft’s 12 public sample Power BI reports, reads every DAX measure, calculation group and calculated table in them, checks each measure against the list on Snowflake’s Power BI ingestion page, follows references from one measure to another, checks its own result and writes dax_measure_status.csv, one row for each of the 271 measures with its status and cause. It needs the duckdb and pbixray Python modules (pbixray brings pandas), no account and no API key, and it does not call Claude. As published it downloads only the 12 pinned Microsoft files. To run it on your own .pbix files, change fetch() to read local paths and drop the pinned sizes and hashes.

The question behind it is the first one in any BI migration: how much of this estate will the converter leave to a person? Snowflake publishes what its Power BI ingestion does not convert. Reading that list against a real model is harder than it looks, because most of the measures it affects never name a listed function. They call another measure that does.

Method

The data

Twelve .pbix files from microsoft/powerbi-desktop-samples, pinned to commit 64ff6b1476050e07a9011adb0894d25c385dc1f1, under the MIT license (“Copyright (c) Microsoft Corporation”; read 2026-10-18). Eight come from the folder 2026 Power BI Samples Revamp and four from Sample Reports (Human Resources, Sales & Returns, Supply Chain, COVID-19 US Tracking). The file list in Run 1 shows each file’s path and byte size. GitHub’s raw file host sends no Last-Modified header, so the script pins each file by byte size and SHA-256 and stops if either differs. A commit never changes, so this guards against a typo or a corrupt download, not against the source moving.

The script reads the files with pbixray, which opens the model inside a .pbix without Power BI. It takes the measures, the calculation groups and their items, the calculated tables and the calculated columns. It reads only model definitions (measures, calculation groups, calculated tables and column names) and no row of report data.

The list

Snowflake’s page on Power BI ingestion feature support (read 2026-10-18) names the DAX it does not convert: KEEPFILTERS and time intelligence (PREVIOUSMONTH, SAMEPERIODLASTYEAR, TOTALYTD); SELECTEDVALUE, ALLSELECTED and ISFILTERED; MEDIAN, PERCENTILE and the STDEV and VAR families; SUMMARIZE, TOPN, RANKX, CONTAINS, ISEMPTY and COUNTROWS over VALUES; and RELATEDTABLE. It also says calculation groups and calculated tables “have no Snowflake table behind them, so they are dropped along with any measure that reads them”.

Databricks is not in the comparison, and the reason is a finding. Its page Import BI files using Genie Code (Microsoft Learn, dated 2026-10-15; read 2026-10-18) gives the accepted file types (.twb, .twbx, .tds, .tdsx and .pbit), a 100 MB limit for direct uploads and the requirement that partner-powered AI features are on. It lists no DAX function that the import cannot handle. We read that one page, so we say only that it gives us nothing to check a model against; you would learn what the import misses by running it.

The mapping table

The script turns that list into a table. Where a row says “Snowflake’s page”, the function is written there. Where it says “Our judgment”, we read a function seen in these reports as belonging to a family the page describes (“functions like” time intelligence, or selection functions). Those four rows are ours, so the script reports the result both ways: listed (Snowflake’s named functions, calculation groups and calculated tables) and broad (listed plus our four rows). Delete a row from MAPPING and the numbers move.

The mapping, with how many measures call each function directly in the 12 reports (from Run 1)
KeyCategoryWhere the row comes fromNoteMeasures that call it
PREVIOUSMONTHTime intelligenceSnowflake’s pagenamed in the time-intelligence sentence4
SAMEPERIODLASTYEARTime intelligenceSnowflake’s pagenamed in the time-intelligence sentence12
TOTALYTDTime intelligenceSnowflake’s pagenamed in the time-intelligence sentence2
KEEPFILTERSFilter contextSnowflake’s pagenamed4
SELECTEDVALUESelectionSnowflake’s pagenamed as describing what the reader clicked9
ALLSELECTEDSelectionSnowflake’s pagenamed as describing what the reader clicked3
ISFILTEREDSelectionSnowflake’s pagenamed as describing what the reader clicked0
MEDIANStatisticalSnowflake’s pagenamed; we also match MEDIANX0
PERCENTILEStatisticalSnowflake’s pagenamed; we match PERCENTILE and PERCENTILEX in their .INC and .EXC forms0
STDEVStatisticalSnowflake’s pagenamed as a family; we match STDEV and STDEVX in their .S and .P forms0
VARStatisticalSnowflake’s pagenamed as a family; we match VAR and VARX in their .S and .P forms (not the VAR keyword)0
SUMMARIZETable functionSnowflake’s pagenamed0
TOPNTable functionSnowflake’s pagenamed0
RANKXTable functionSnowflake’s pagenamed4
CONTAINSTable functionSnowflake’s pagenamed0
ISEMPTYTable functionSnowflake’s pagenamed0
COUNTROWSTable functionSnowflake’s pagenamed only as COUNTROWS over VALUES; matched on that text pattern0
RELATEDTABLERelated tableSnowflake’s pagenamed0
DATEADDTime intelligenceOur judgmentour reading of “functions like”1
DATESINPERIODTime intelligenceOur judgmentour reading of “functions like”1
ISINSCOPESelectionOur judgmentour reading of the selection sentence3
ISCROSSFILTEREDSelectionOur judgmentour reading of the selection sentence4

Eleven of the functions Snowflake names are never called in these 12 reports (their count is 0). Those rows are in the table but were never exercised by these files, so treat their matching as untested here.

The steps

  1. Strip string literals and comments from each expression, so a function name inside text is not counted.
  2. Find the functions the expression calls, and match them against the table. VAR the keyword is not the VAR.S function.
  3. Find what it reads: references to other measures ([Profit]), columns ('Sales'[Amount]), calculation-group tables and calculated tables. Power BI’s own date tables, the ones it makes for date columns, are not counted as calculated tables.
  4. Follow measure-to-measure references to any depth. A measure that calls nothing on the list, but builds on one that does, is Indirect. We assume it is affected too. Snowflake states that only for calculation groups and calculated tables, so this step is our reading of how metrics build on each other.
  5. Give each measure one status, in this order: Direct function, Calculation group, Calculated table, Indirect, Clear.

The resolver in step 4 is short. This is the function from the script, as published:

Python: reach every cause through the measures a measure references (excerpt of the script)
def reach(sample, name, allowed, seen=()):
    """Causes reachable from a measure through the measures it references, with the path."""
    out = {}
    for _r, res in by_name.get((sample, name), []):
        if res is None:
            continue
        causes, refs, _ = res
        for c in causes:
            if c[0] in allowed:
                out.setdefault(c, [name])
        for ref in refs:
            if ref in seen or ref == name:
                continue
            for c, p in reach(sample, ref, allowed, seen + (name,)).items():
                out.setdefault(c, [name] + p)
    return out

The script checks its own result

Before it prints a result table or writes the CSV, the script runs six checks. Each is listed here with the planted fault that trips it (Run 2 plants all of them):

  • Rows loaded equal rows read from the files. Tripped by a duplicated or invented row.
  • No measure appears twice in one report. Tripped by a duplicated row.
  • Every measure has an expression. Tripped by a missing value.
  • Every reference resolves to a measure or a column. Tripped by a reference to a measure that does not exist.
  • Every key Snowflake names appears in Snowflake’s text. The script holds the page’s sentences. Tripped by a typo in a key.
  • Every key we added is a function the reports actually call. Tripped by a typo in one of our rows.

The last two are not the check “every key matches a real input”, because eleven of Snowflake’s keys legitimately match nothing in these reports. A key that matches no input is not silently routed anywhere in this script, since a measure with no match is simply Clear. After a clean run it also reads the CSV back and checks that it holds as many rows as the source and that its status shares add to 100%. That check guards the writing step, and none of the planted faults trips it. If any check fails, the script prints the failure, writes no CSV and exits with code 1.

Run 1. The measurement

Run as published (python3 dax_converter_gaps.py), the six checks passed and the script wrote the CSV. First, the files it fetched:

Output from our run: the pinned source and the 12 files
Source: microsoft/powerbi-desktop-samples, commit 64ff6b1476050e07a9011adb0894d25c385dc1f1, MIT license
Mode: RUN 1, the measurement
Versions: python 3.14.8, duckdb 1.5.6, pbixray 0.15.5, pandas 3.0.6
file                                                               bytes  last-modified   sha256 (first 12)
2026 Power BI Samples Revamp/AdventureWorks Sales.pbix           8390782  not sent        76c89b64cd1c
2026 Power BI Samples Revamp/Artificial Intelligence Sample.pbix   1120296  not sent        170a12914593
2026 Power BI Samples Revamp/Competitive Marketing Analysis.pbix   6612086  not sent        00157525c8c8
2026 Power BI Samples Revamp/Corporate Spend.pbix                 716704  not sent        2e56785790d2
2026 Power BI Samples Revamp/Employee Hiring and History.pbix    8483276  not sent        0763c8390a71
2026 Power BI Samples Revamp/Regional Sales Sample.pbix          1018743  not sent        050e6b8549c2
2026 Power BI Samples Revamp/Revenue Opportunities.pbix           245930  not sent        3c95c4fb5a26
2026 Power BI Samples Revamp/Store Sales.pbix                    9701642  not sent        160b5b48b74e
Sample Reports/Human Resources Sample PBIX.pbix                  8700848  not sent        f2de1fa89ef6
Sample Reports/Sales & Returns Sample v201912.pbix               6530381  not sent        c0a51fbae6c9
Sample Reports/Supply Chain Sample.pbix                          1177398  not sent        dd55cac1348f
Sample Reports/COVID-19 US Tracking Sample.pbix                  6917621  not sent        3f0410a4fc09
Total downloaded: 59,615,707 bytes in 12 files

Then the checks and the results:

Output from our run: checks, mapping hits, then results
Checks
  [pass] rows loaded = rows pbixray reads from the files: loaded 271, source 271  (tripped by: a duplicated or invented row)
  [pass] no measure appears twice in one report: 0 duplicated name(s)  (tripped by: a duplicated row)
  [pass] every measure has an expression: 0 without one  (tripped by: a missing value)
  [pass] every reference resolves to a measure or column: 0 unresolved   (tripped by: a reference to a measure that does not exist)
  [pass] every vendor-named key is in the vendor's text: not found: none  (tripped by: a typo in a vendor-named key)
  [pass] every family key is a function seen in the reports: not seen: none  (tripped by: a typo in a family key)

Keys of the mapping table and where they matched (all 12 reports)
  key                 category          basis    measures hit directly
  PREVIOUSMONTH       Time intelligence named                        4
  SAMEPERIODLASTYEAR  Time intelligence named                       12
  TOTALYTD            Time intelligence named                        2
  KEEPFILTERS         Filter context    named                        4
  SELECTEDVALUE       Selection         named                        9
  ALLSELECTED         Selection         named                        3
  ISFILTERED          Selection         named                        0
  MEDIAN              Statistical       named                        0
  PERCENTILE          Statistical       named                        0
  STDEV               Statistical       named                        0
  VAR                 Statistical       named                        0
  SUMMARIZE           Table function    named                        0
  TOPN                Table function    named                        0
  RANKX               Table function    named                        4
  CONTAINS            Table function    named                        0
  ISEMPTY             Table function    named                        0
  COUNTROWS           Table function    named                        0
  RELATEDTABLE        Related table     named                        0
  DATEADD             Time intelligence family                       1
  DATESINPERIOD       Time intelligence family                       1
  ISINSCOPE           Selection         family                       3
  ISCROSSFILTERED     Selection         family                       4
  (a key with 0 hits is named by the vendor but not used by any of these 12 reports)

By report: measures that need a person (status other than Clear)
  report                             measures  listed   share  broad   share  missed by direct scan
  AdventureWorks Sales                     20      10   50.0%     10   50.0%                      9
  Artificial Intelligence Sample           29      15   51.7%     15   51.7%                      8
  COVID-19 US Tracking Sample              11       1    9.1%      1    9.1%                      0
  Competitive Marketing Analysis           22       8   36.4%      9   40.9%                      5
  Corporate Spend                          19      19  100.0%     19  100.0%                     17
  Employee Hiring and History              25      15   60.0%     15   60.0%                     10
  Human Resources Sample PBIX              30      15   50.0%     15   50.0%                     10
  Regional Sales Sample                    15      10   66.7%     10   66.7%                      7
  Revenue Opportunities                     6       0    0.0%      0    0.0%                      0
  Sales & Returns Sample v201912           58      46   79.3%     46   79.3%                     35
  Store Sales                              32       0    0.0%      0    0.0%                      0
  Supply Chain Sample                       4       0    0.0%      0    0.0%                      0
  ALL 12 REPORTS                          271     139   51.3%    140   51.7%                    101

By status (listed = Snowflake's named functions, calculation groups and calculated tables; broad adds the family rows)
  status                listed   share  broad   share
  Direct function           38   14.0%     44   16.2%
  Calculation group          9    3.3%      9    3.3%
  Calculated table           4    1.5%      3    1.1%
  Indirect                  88   32.5%     84   31.0%
  Clear                    132   48.7%    131   48.3%
  Total                    271  100.0%    271  100.0%

Direct function hits by category (a measure counts once per category)
  Time intelligence      20
  Selection              18
  Filter context          4
  Table function          4

Objects that are not measures (Snowflake drops calculation groups and calculated tables)
  report                             calc groups calc items calc tables auto date calc columns
  AdventureWorks Sales                         2         13           0         0            4
  Artificial Intelligence Sample               0          0           3         0            9
  COVID-19 US Tracking Sample                  0          0           0         2           16
  Competitive Marketing Analysis               0          0           1         0           10
  Corporate Spend                              0          0           2         0            2
  Employee Hiring and History                  0          0           0         0            7
  Human Resources Sample PBIX                  0          0           0         6           43
  Regional Sales Sample                        0          0           3         0            5
  Revenue Opportunities                        0          0           1         0            1
  Sales & Returns Sample v201912               0          0           1         2           15
  Store Sales                                  0          0           2         0            7
  Supply Chain Sample                          0          0           0         1            8
  ALL 12 REPORTS                               2         13          13        11          127
  (auto date = tables Power BI makes itself for date/time columns; not counted as calc tables)

CSV written: dax_measure_status.csv, 271 rows (source 271); status shares sum to 100.0%: pass

Result table: measures that need a person, by report

The same numbers as the run, using the listed view. “Found by searching” counts measures that call a listed function themselves. The rest of the flagged measures reach a cause only through other measures, a calculation group or a calculated table.

ReportMeasuresFound by searching for the listed functionsFlagged after following referencesShare flaggedMissed by the search
AdventureWorks Sales2011050.0%9
Artificial Intelligence Sample2971551.7%8
COVID-19 US Tracking Sample11119.1%0
Competitive Marketing Analysis223836.4%5
Corporate Spend19219100.0%17
Employee Hiring and History2551560.0%10
Human Resources Sample PBIX3051550.0%10
Regional Sales Sample1531066.7%7
Revenue Opportunities6000.0%0
Sales & Returns Sample v20191258114679.3%35
Store Sales32000.0%0
Supply Chain Sample4000.0%0
All 12 reports2713813951.3%101

Download: dax_measure_status.csv (columns: report, sample set, table, measure, status under the listed and broad views, causes, the chain of measures to follow, whether a direct search finds it, and the expression length).

Result table: by status

StatusMeasuresShare
Direct function3814.0%
Calculation group93.3%
Calculated table41.5%
Indirect8832.5%
Clear13248.7%
Total271100.0%

This is the listed view. The broad view, which adds our four rows, flags 140 of 271 (51.7%), so those rows change little here.

What else the reports hold

Snowflake drops calculation groups and calculated tables, so the script counts them too.

ReportCalculation groupsCalculation itemsCalculated tablesAuto date tablesCalculated columns
AdventureWorks Sales213004
Artificial Intelligence Sample00309
COVID-19 US Tracking Sample000216
Competitive Marketing Analysis001010
Corporate Spend00202
Employee Hiring and History00007
Human Resources Sample PBIX000643
Regional Sales Sample00305
Revenue Opportunities00101
Sales & Returns Sample v201912001215
Store Sales00207
Supply Chain Sample00018
All 12 reports2131311127

What the numbers say

  • A text search misses most of the problem. Searching for the listed functions finds 38 of 271 measures (14.0%). Following references, calculation groups and calculated tables flags 139 (51.3%), so 101 flagged measures contain no listed function themselves.
  • One base measure can flag a whole report. In Corporate Spend every one of 19 measures is flagged. One measure, Amount, calls TOTALYTD; 17 others build on it, and the via column in the CSV shows each chain. The 19th, IT Area/Sub Area, calls SELECTEDVALUE itself. Under our indirect rule, rewriting the base measure would clear the dependents.
  • The spread between reports is wide. Revenue Opportunities, Store Sales and Supply Chain have no flagged measure; Sales & Returns has 46 of 58 (79.3%). The average hides this, so run it per report before sizing a migration.
  • Calculation groups sit in one report. AdventureWorks Sales has two calculation groups with 13 items. Nine of its measures read them, and 10 of its 20 measures are flagged. Six of the nine read the Time Intelligence group, whose items call SAMEPERIODLASTYEAR and the DATESMTD, DATESQTD and DATESYTD functions, so those measures never show it themselves. The other three read a Date Role group that switches relationships with USERELATIONSHIP. Both facts come from reading the items in the pinned file, not from a run file.
  • Time intelligence and selection functions lead. Of the 44 measures that call a function directly in the broad view (38 in the listed view), 20 call a time-intelligence function themselves and 18 a selection function (a measure counts once per category, and some have both).

For the decision “migrate, rebuild, keep or retire”, the CSV gives a sorting aid and not a verdict. A report with no flagged measure is a candidate for the platform converter’s first pass plus a parity test. A report with many Indirect measures is cheaper than its flagged share suggests if the causes sit in a few base measures. Which reports to retire depends on usage, which these files do not hold. Snowflake’s page also gives a second reason a measure is not converted: it is “not valid as a semantic-view metric”, because a metric must be a single aggregate over its own table’s rows. We did not test that rule, so for the question of where a metric should live, the flagged share is a floor, not a forecast.

Run 2. Planted faults the checks must catch

The same script with --plant adds five rows to the AdventureWorks Sales measures after reading them: a copy of the measure Cost; Planted Sales PY, which calls SAMEPERIODLASTYEAR; Planted PY Margin, which reaches it only through Planted Sales PY; Planted Broken Ref, which references a measure that does not exist; and Planted Empty, which has no expression. It also adds two misspelled keys to the mapping table. The files themselves are untouched. The script must report the faults and refuse to write a CSV.

Output from our run: python3 dax_converter_gaps.py --plant
Checks
  [FAIL] rows loaded = rows pbixray reads from the files: loaded 276, source 271  (tripped by: a duplicated or invented row)
  [FAIL] no measure appears twice in one report: 1 duplicated name(s)  (tripped by: a duplicated row)
  [FAIL] every measure has an expression: 1 without one  (tripped by: a missing value)
  [FAIL] every reference resolves to a measure or column: 1 unresolved AdventureWorks Sales: Planted Broken Ref -> [No Such Measure]  (tripped by: a reference to a measure that does not exist)
  [FAIL] every vendor-named key is in the vendor's text: not found: SAMEPERIODLASTYAER  (tripped by: a typo in a vendor-named key)
  [FAIL] every family key is a function seen in the reports: not seen: ISINSCOP  (tripped by: a typo in a family key)

Planted measures, as the resolver sees them
  Planted Broken Ref   status=Clear            via=-                  a direct-call scan finds it: no
  Planted Empty        status=Unscorable       via=-                  a direct-call scan finds it: no
  Planted PY Margin    status=Indirect         via=planted sales py   a direct-call scan finds it: no
  Planted Sales PY     status=Direct function  via=-                  a direct-call scan finds it: yes

STOPPED: 6 check(s) failed. No result table and no CSV from this run.

All six checks failed and the script exited with code 1, so it wrote no result table and no CSV. The planted-measure lines show the case the whole page is about. Planted PY Margin never names a time-intelligence function, so a direct search does not find it (a direct-call scan finds it: no), but the resolver reports it as Indirect, through planted sales py. The broken reference is the opposite case: the resolver calls Planted Broken Ref Clear, because it can see nothing on the list in it, and only the reference check catches it. That is why the script does not rely on one check.

Limits

  • This is a prediction from a list, not a converter test. We did not run Snowflake’s or Databricks’ converter. A function on the list may convert in a simple case, and a measure we call Clear may still fail for a reason the page does not list. Run the converter on your own file to know.
  • It is not a client result. These are Microsoft’s demonstration reports, built to show Power BI features. Real estates differ, and the shares above say nothing about yours.
  • Twelve reports, 271 measures. It is a small, convenient sample from one publisher. Do not read 51.3% as an industry rate.
  • The vendor’s list is the standard, and the vendor can change it. We read it on 2026-10-18. Snowflake’s page also says “functions like” for time intelligence. Our four family rows are one reading of that; another reader would add more.
  • The indirect rule is our assumption. Snowflake states that measures reading a calculation group or calculated table are dropped. That a measure depending on a flagged measure is also affected is our reading, though a dependent measure cannot be built if the measure under it is missing.
  • Only the model is read. Snowflake says measures saved in the report rather than in the data model are not read, and we read only model measures too. Tableau files, Tableau’s LOD expressions and row-level security are out of scope.
  • Other parts of the Snowflake list were not tested: the single-aggregate rule for semantic-view metrics, data entered directly in Power BI, and tables that are not Snowflake-backed. The flagged share is therefore a floor for the reasons Snowflake publishes.
  • Databricks has no column here. The import page we read lists no functions, so we say nothing about what its import misses.
  • The parser is ours. It handles the expressions in these 12 files and the checks flag any reference it cannot resolve, but DAX has more syntax than these files use.

Tested on

Tested on

Platform
DuckDB 1.5.6, run from Python with pbixray and pandas (versions are printed in the first line of the run output). No Snowflake or Databricks converter was run.
Edition
Open-source DuckDB library (pip install duckdb pbixray)
Date
2026-10-18
Dataset
12 sample reports from microsoft/powerbi-desktop-samples, commit 64ff6b1476050e07a9011adb0894d25c385dc1f1, MIT license, each file pinned by byte size and SHA-256
List
Snowflake: Power BI ingestion feature support (read 2026-10-18)
Code
dax_converter_gaps.py
Result
dax_measure_status.csv

The script pins the commit and every file, sorts its output and uses no random numbers, so another run on the same files gives the same numbers. Run it with python3 dax_converter_gaps.py (the measurement) or add --plant (the planted faults). Downloads land in loop-out/data/dax-samples unless you pass --data-dir.

When this is not enough

  • You need to know what the converter really does with your reports. This predicts from a list. Only a run of the converter on your file shows what it converts, and a parity test shows whether the converted numbers match the old ones.
  • Your reports are Tableau, or sit on Databricks. The method carries over, but the Databricks import page we read lists no functions to check against, and Tableau workbooks need their own parser.
  • You must decide what to retire. Usage logs, owners and overlap decide that, and no file in a report holds them.
  • Your measures live in the report, not the model. Snowflake’s page says it does not read them, and neither does this script.

Find out what your own reports would need

These numbers come from Microsoft’s sample reports, not yours. Our fixed-fee migration assessment starts with your platform’s own converter. Claude then fills what it misses and writes a parity test for every measure.

Your report owners sign off before the old report is switched off. Your data must already be on Databricks or Snowflake. The script in this guide only downloads the 12 sample files, so it needs adapting before it can read yours.

See the migration assessment