Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 10/28/2026
ms.date: 10/29/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand All @@ -18,14 +18,16 @@ ms.reviewer: brettwil

This article summarizes the features and enhancements in each release of the FinOps toolkit.

<!--
<br>

## Unreleased

The following section lists features and enhancements that are currently in development.

-->
### [Power BI reports](power-bi/reports.md)

- **Fixed**
- Fixed the storage-based Costs query failing to refresh with `Type mismatch (DISP_E_TYPEMISMATCH 0x80020005)` when a row's SKU details didn't carry a clean numeric VCPUs/vCores value (for example, a Marketplace or reservation purchase row). The value is now coerced to null instead of throwing ([#2297](https://github.com/microsoft/finops-toolkit/issues/2297)).

<br><a name="latest"></a>

Expand Down
2 changes: 1 addition & 1 deletion src/power-bi/queries/ftk_NormalizeSchema.pq
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ let
{"UsageType", "ImageType", "ServiceType", "VMName", "VMProperties", "VCPUs", "ReservationOrderId", "ReservationId", "VMCapacityReservationId", "AHB", "vCores"},
{"x_SkuUsageType", "x_SkuImageType", "x_SkuType", "tmp_VMName", "x_SkuVMProperties", "tmp_VMvCPUs", "tmp_AddlReservationOrderId", "tmp_AddlReservationId", "x_CapacityCommitmentId", "tmp_SQLAHB", "tmp_SQLvCores"}
),
SkuDetailsDict_types = Table.TransformColumnTypes(SkuDetailsDict_expand, {{"tmp_VMvCPUs", Int64.Type}, {"tmp_SQLvCores", Int64.Type}}),
SkuDetailsDict_types = Table.TransformColumns(SkuDetailsDict_expand, {{"tmp_VMvCPUs", each try Int64.From(_) otherwise null, Int64.Type}, {"tmp_SQLvCores", each try Int64.From(_) otherwise null, Int64.Type}}),

// Columns from AddlInfo -- Must be after ResourceName cleanup
ResourceMachineName = _swapCol(SkuDetailsDict_types, "tmp_VMName", "x_ResourceMachineName", each if _isNotBlank([tmp_VMName]) then [tmp_VMName] else null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1685,14 +1685,15 @@ table Costs
// TODO: Add SQL AHB handling
AHB = Table.RemoveColumns(
Table.AddColumn(Table.AddColumn(Table.AddColumn(Table.AddColumn(Table.AddColumn(Table.AddColumn(Table.AddColumn(
Table.TransformColumnTypes(
Table.TransformColumns(
Table.ExpandRecordColumn(
Table.ReplaceErrorValues(Table.AddColumn(ExtractedTags, "tmp_SkuDetails", each [x_SkuDetailsDictionary]), {{"tmp_SkuDetails", null}}),
"tmp_SkuDetails",
{"UsageType", "ImageType", "ServiceType", "VMName", "VMProperties", "VCPUs", "ReservationOrderId", "ReservationId", "VMCapacityReservationId", "AHB", "vCores", "ServerSku"},
{"x_SkuUsageType", "x_SkuImageType", "x_SkuType", "tmp_VMName", "x_SkuVMProperties", "tmp_VMvCPUs", "tmp_AddlReservationOrderId", "tmp_AddlReservationId", "x_CapacityCommitmentId", "tmp_SQLAHB", "tmp_SQLvCores", "tmp_ServerSku"}
),
{{"tmp_VMvCPUs", Int64.Type}, {"tmp_SQLvCores", Int64.Type}}
// Coerce to Int64, degrading any non-numeric/unexpected VCPUs/vCores value (e.g. on a Marketplace, reservation, or other non-VM/SQL charge row) to null instead of throwing a type mismatch error on refresh
{{"tmp_VMvCPUs", each try Int64.From(_) otherwise null, Int64.Type}, {"tmp_SQLvCores", each try Int64.From(_) otherwise null, Int64.Type}}
),
"x_ResourceMachineName", each if _isNotBlank([tmp_VMName]) then [tmp_VMName] else null),
"x_SkuCoreCount", each if [tmp_VMvCPUs] <> null then [tmp_VMvCPUs] else if [tmp_SQLvCores] <> null then [tmp_SQLvCores] else null, Int64.Type),
Expand Down
40 changes: 40 additions & 0 deletions src/powershell/Tests/Lint/PowerQuerySkuDetailsCasts.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
Lint rule: values extracted from the free-form x_SkuDetails JSON blob must not be cast with an unguarded
Table.TransformColumnTypes.

The blob's shape varies by charge type (VM usage, Marketplace, reservation purchase, etc.), so VCPUs / vCores
are not always a clean number. An unguarded cast produces an Error cell, which Power BI surfaces on refresh as
"OLE DB or ODBC error: Type mismatch (DISP_E_TYPEMISMATCH 0x80020005)" (issue #2297). Nothing in CI runs
Power Query, so this static check keeps the guarded form (try ... otherwise null) from regressing.
#>

Describe 'PowerQuerySkuDetailsCasts' {

BeforeDiscovery {
$repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path
$files = @(
Get-ChildItem -Path (Join-Path $repoRoot 'src/power-bi') -Include '*.tmdl', '*.pq' -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { (Get-Content -Path $_.FullName -Raw) -match 'tmp_VMvCPUs|tmp_SQLvCores' } |
ForEach-Object { @{ FullName = $_.FullName; RelPath = $_.FullName.Substring($repoRoot.Length + 1).Replace('\', '/') } }
)
}

It 'Finds at least one dataset that extracts VCPUs/vCores' {
$files.Count | Should -BeGreaterThan 0
}

It 'Does not cast tmp_VMvCPUs / tmp_SQLvCores with an unguarded Table.TransformColumnTypes in <RelPath>' -ForEach $files {
$text = Get-Content -Path $FullName -Raw
$unguarded = [regex]::Matches($text, '\{\s*"tmp_(VMvCPUs|SQLvCores)"\s*,\s*Int64\.Type\s*\}')
$unguarded.Count | Should -Be 0 -Because 'an unparseable VCPUs/vCores value must degrade to null (try ... otherwise null), not fail the refresh'
}

It 'Guards the tmp_VMvCPUs / tmp_SQLvCores cast with try ... otherwise null in <RelPath>' -ForEach $files {
$text = Get-Content -Path $FullName -Raw
$text | Should -Match '"tmp_VMvCPUs",\s*each try [^,]+ otherwise null'
$text | Should -Match '"tmp_SQLvCores",\s*each try [^,]+ otherwise null'
}
}
Loading