Hyperscale log file reads n/a (log service) and stays out of allocated database sizes - #4880
Merged
Merged
Conversation
… leave it out of allocated totals On Azure SQL Database Hyperscale, sys.database_files reports the LOG file at about 1 TB (1,046,528 MB) while the data file reports its real allocation. The log lives in Hyperscale's log service, so that figure is not storage the database holds or pays for (Hyperscale bills allocated data storage). Database Sizes in both apps showed the database at 1,056,768 MB allocated against 315 MB used. The Azure SQL Database branch of the database_size_stats collector now detects Hyperscale from DATABASEPROPERTYEX(DB_NAME(), 'Edition') and stores a NULL size, growth and ceiling for the LOG row only. The data file keeps its size, and used space and the VLF count stay as collected. SQL Server, Managed Instance and non-Hyperscale Azure SQL Database are unchanged. Lite's DuckDB store drops NOT NULL from database_size_stats.total_size_mb (schema v67); Darling's Postgres store already held it nullable. Every reader in both apps passes the NULL through instead of reading it as 0: the Database Sizes grid words it as n/a (log service) and sorts it last, the allocated and free totals, the cost share and the utilization chart leave it out, and get_database_sizes returns null with a short note in both apps. The web Database Sizes table shows that note.
erikdarlingdata
marked this pull request as ready for review
September 30, 2026 22:40
erikdarlingdata
marked this pull request as draft
September 30, 2026 22:50
…-day sizes too Rows stored before the collector wrote NULL for the Hyperscale log row still hold its ~1 TB. With only the latest sum leaving the log out, a Hyperscale database read as a drop of about 99%. - One predicate leaves out any file whose row in the latest snapshot has no size. Both apps' Storage Growth queries apply it to the latest, 7-day and 30-day sums alike. It runs at read time, so the old rows need no migration. - A file that is gone from the latest snapshot has no row there, so it still counts on the past side, as shrinkage. - FileIoStatsCollector.NoSizeLabel points at HyperscaleLogSize.Display, so one constant owns "n/a (log service)". - The get_database_sizes note, which the web Database Sizes table shows, is in plain words and names no field.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
On Azure SQL Database Hyperscale, Database Sizes now shows the log file's size as "n/a (log service)" and leaves the log out of every allocated total. The data file keeps its real size. Storage Growth leaves the log file out of its current, 7-day, and 30-day sizes, so its growth is the data file's own. SQL Server, Managed Instance, and other Azure SQL Database tiers do not change. The fix covers Lite and Darling (the service, the viewer, and the web page).
Which component(s) does this affect?
Why
A Hyperscale database keeps its transaction log in a separate log service.
sys.database_filesstill lists a LOG row for it, sized at about 1 TB (1,046,528 MB). The Hyperscale FAQ gives that 1 TB as the cap on the active log. It is not storage that the database holds. The Hyperscale service tier page says you pay for storage based on actual allocation. It also says storage is allocated automatically between 10 GB and 128 TB: https://learn.microsoft.com/en-us/azure/azure-sql/database/service-tier-hyperscaleBefore this change, one Hyperscale database showed about 1,056,768 MB allocated against 315 MB used. That total is the 10,240 MB data file plus the 1,046,528 MB log row. The log row made the allocation look about 100 times too large.
What changes
The collector (shared by Lite and Darling):
DATABASEPROPERTYEX(DB_NAME(), N'Edition') = N'Hyperscale'.df.type = 1) returns NULL fortotal_size_mb,max_size_mb,auto_growth_mb,growth_pct, andis_percent_growth. The max size and growth settings are log-service values like the 1 TB size, and no reader needs them.used_size_mbandvlf_countstay as collected. The data file row is unchanged.The stores:
total_size_mbwas NOT NULL, so DuckDB rejected the NULL row. Schema version 67 drops that constraint. DuckDB refuses the change while the table has an index, so the migration dropsidx_database_size_stats_timefirst, as version 57 does. The startup index pass recreates it.Every reader, in both apps:
PerformanceMonitor.Common(HyperscaleLogSize), so both apps use the same text:n/a (log service).FileIoStatsCollector.NoSizeLabel, the File I/O text for the same file, now points atHyperscaleLogSize.Display, so one constant owns the words.TargetNullValue). The free space and used percent of that row are blank. Reads no longer turn a NULL size into 0.NULLS LAST. PostgreSQL sorts a null first underDESC.get_database_sizesMCP tool (Lite and Darling): the per-filetotal_size_mb,auto_growth_mb, andmax_size_mbare null for that row. The database total and used figures leave the file out. The payload adds a top-levelnoteonly when a null exists, so every other server returns the same shape as before.Storage Growth (Lite
LocalDataService.StorageGrowthSql, Darling viewerViewerDataService.StorageGrowthSql):collection_time($2) as the latest sum, so the query still binds only literal snapshot times.Not touched:
deprecated/and the install scripts.Why this predicate
There were two choices. One leaves out a file whose latest row has no size. The other leaves out a LOG file on a Hyperscale database. This PR uses the first.
database_size_stats, the table that Storage Growth already reads, and it keys on the NULL that the collector writes for this one row.database_size_statsdoes not store the edition. A LOG-plus-Hyperscale predicate must joinserver_propertiesand match its edition text.Neither choice covers one case. A database that moved to Hyperscale in the last 30 days has old log rows with a real allocation. Both predicates leave those rows out, so the release of that log allocation does not show as shrinkage.
Other readers of file sizes
Only the two Storage Growth queries compare a past sum with the latest one. These readers were checked, and they need no change:
get_database_sizes(both apps): the latest snapshot only.LocalDataService.FinOps.ServerProperties.cs,ViewerDataService.FinOps.Inventory.cs) and the idle-database list: the latest snapshot only.LocalDataService.FileGrowth.cs,DarlingAlertReadAdapter.cs): each file is compared with its own earlier row, and a file whose current size is NULL is skipped. Both sides drop the log file together.database_size_stats. The projections that exist are for PostgreSQL targets and for the Darling store's own disk.How was this tested?
No SQL Server, Azure SQL Database, or PostgreSQL connection was used. The checks are unit tests and real DuckDB files. The Hyperscale edition string was not checked against a live Hyperscale database (see "Still to check").
Test plan
dev. That build includes Lite, the Darling service and viewer, the collectors, and the common project.Lite.Tests/HyperscaleLogSizeTests.cs(16 tests),Darling/Darling.Tests/HyperscaleLogSizeTests.cs(10 tests), one upgrade test inAgedDatabaseMigrationTests, and one relaxation recorded inDuckDbSchemaEquivalenceTests.HyperscaleLogSize*andFileIoHyperscaleLogSizeTests, 18 tests, 0 failed. DarlingHyperscaleLogSizeTestsandFileIoHyperscaleLogSizeTests, 15 tests, 0 failed. DarlingViewerFinOpsSqlTestsandDarlingMcpObjectStatsToolsSurfaceAndSqlTests, 72 tests, 0 failed.Lite.Testssuite on the final head: 6,452 tests, 0 failed, 0 skipped.Darling.Testssuite on the final head: 19,077 tests, 0 failed, 1,197 skipped, 1 not run. The skips are live PostgreSQL classes, which need a store. The one not run is an explicit-only test (McpSchemaCompatServiceLeakRaceTests).DatabaseSizeLatestPlanShapeLiveTestschanged only so that it compiles with a nullable size. Its seed has no NULL size, so the Storage Growth predicate leaves its results unchanged.Red lines (the new tests fail when the behavior is reverted):
How the red run was made: these pieces went back to their old form. They were the collector query and read, the two DuckDB schema files, and every reader's null handling. They also included the MCP note branch, the
NULLS LASTsort, the chart sum, bothTargetNullValuebindings, and the webnoteargument. The row model stayed nullable so the test projects still compile.Red lines for the Storage Growth predicate. Each run removed the predicate from one sum, rebuilt, and ran the Storage Growth tests:
StorageGrowth_AFileGoneFromTheLatestSnapshot_StillCountsAsShrinkage_AndARealLogCountsandStorageGrowth_LatestSnapshotStillFromBeforeTheChange_CountsTheLogRowOnBothSides_LikeTheDatabaseSizesGridpass on the old code too. They guard behavior the predicate must keep. A dropped file still counts, and a log row with a real size and the samefile_idstill counts. A latest snapshot from before the upgrade still shows the data file's growth.Tests that cannot fail on the old behavior, and why:
OnPrem_Query_IsUnchanged_NoHyperscaleBranchandOnPremQuery_IsUnchanged_NoHyperscaleBranchguard behavior that must not change, so they pass before and after.PostgresStore_KeepsTotalSizeNullableandGetDatabaseSizesPayload_NoLogServiceFile_HasNoNote_AndTheSameShapeAsBefore(both apps) also guard behavior that must not change.Row_WithoutAnAllocation_ReadsAsNotApplicable_WithoutThrowing,TheSharedNote_NamesTheWordsTheGridShows,ViewerRow_HyperscaleLogRow_ReadsAsNotApplicable_AndOutOfTheAllocatedTotals_WhileTheDataRowCounts, and the two payload-shape tests use members that only this change adds: a nullable size,AllocatedTotalMb,FreeTotalMb,HyperscaleLogSize, andDatabaseSizesPayload. On the old code they do not compile, so they were not run red.Still to check
DATABASEPROPERTYEXpage lists theseEditionvalues: General Purpose, Business Critical, Basic, Standard, Premium, System, FabricSQLDB, and NULL. It does not list Hyperscale. The same tier name is documented forsys.database_service_objectives.editionand for the PowerShellEditionvalue. That view needs thedbmanagerrole, so the collector cannot rely on it. The query uses theDATABASEPROPERTYEXform. If a real Hyperscale database returns a different string, the log row keeps its old size. The new tests still pass, because they pin the query text. One run ofSELECT DATABASEPROPERTYEX(DB_NAME(), N'Edition')on a Hyperscale database settles it.Checklist
dotnet build -c Debug)CHANGELOG
SECTION: Fixed
ENTRY:
get_database_sizesMCP tool returns a null size for that file and adds a note. The Darling web page shows that note above the Database Sizes table. Lite drops a NOT NULL constraint on that size column at its next start so the row can be saved.REF:
[Hyperscale log file reads n/a (log service) and stays out of allocated database sizes #4880]: Hyperscale log file reads n/a (log service) and stays out of allocated database sizes #4880