Repository navigation
query_store: attribute replica role on Azure SQL DB, on live GP + Hyperscale evidence - #1884
Conversation
…dinal comment The #1836 gate held replica attribution off for Azure SQL DB on bind safety grounds, and named the exact live probe that would lift it. That probe has now been run on both service tiers the gate worried about, 2026-07-31 UTC, both on Microsoft SQL Azure (RTM) 12.0.2000.8 / EngineEdition 5: - General Purpose GP_S_Gen5_1 (#1848) and Hyperscale HS_S_Gen5_2 (#1872) each returned OBJECT_ID(sys.query_store_replicas) = -660 and COL_LENGTH(sys.query_store_runtime_stats, replica_group_id) = 8. - sys.query_store_replicas binds on both, holding the same 4 role rows box SQL Server has; every runtime-stats row carries replica_group_id = 1 and LEFT JOINs cleanly to Primary. - Decisive: on Hyperscale the full 55-column payload composed with the attribution ON bound 55 of 55 columns, exit 0, replica_role = Primary. So hasReplicaAttribution becomes productVersion >= 16 || IsAzureSqlDb, the same "Azure means newest" rule hasPlanType already uses. A per-tier gate was rejected as unbuildable: Hyperscale reports the same engine edition as General Purpose. Managed Instance deliberately keeps the pure version gate. It under-reports PRODUCTVERSION too, but its feature set follows a per-instance update policy rather than being evergreen, and it has no per-database path to verify on. No read-side change is needed: every Query Store dedup key and rollup already partitions on replica_role (Lite LocalDataService.QueryStore, the Darling viewer and MCP readers, ComposeCompiler, and the TimescaleSupport CAGG), so an Azure server goes from one NULL group to one Primary group and the grids are unchanged. Tests: the Azure pin is deliberately inverted, with the evidence cited in place so a future reader does not "restore" it; a second Azure pin covers the failed-probe path, since the edition alone must decide it; MI now pins replica_role OFF beside its existing plan_type pin. Pre-2022 and 2017 on-prem shapes and the shared-body drift guard are untouched and still green. Also #1871: BuildPayloadBody's summary said the two paths select "53 reader ordinals" when the real count is 55 - the class doc and the reader already agreed on 55, and the count was not updated when #1841 added the interval-identity pair. That comment is the drift guard between the two paths, so the number matters. The Row.ReplicaRole doc, which named Azure as a NULL source, is corrected too. The repo carries no other stale 53 column-count reference; the remaining 53s are real reader ordinals. Closes #1872 Closes #1871 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ReviewReviewed the diff ( OverviewFlips Correctness
Test coverage
Lite/Darling parityNo parity drift: Security / Performance
Minor noteNot a blocker, but the Overall: correct, well-tested, no parity drift, no security or performance concerns. |
…-attribution # Conflicts: # CHANGELOG.md
ReviewReviewed the diff ( Correctness
Parity (Lite/Darling)
Security / Performance
Minor / non-blocking
Test coverage for the new behavior looks solid: the inverted pin explicitly documents why it flipped and warns against a bare re-flip, and the new No missing-index DMV concerns apply here (not that kind of change). Nothing else stood out as a correctness, parity, security, or performance risk. |
…-attribution # Conflicts: # CHANGELOG.md
| update policy rather than being evergreen, so "Azure means 2022+" is not sound there and MI | ||
| has no per-database path to verify it on. IsAzureSqlDb is false for MI, so MI falls through | ||
| to productVersion >= 16 and stays off until a probe on MI says otherwise. */ | ||
| bool hasReplicaAttribution = productVersion >= 16 || context.Target.IsAzureSqlDb; |
There was a problem hiding this comment.
Risk: this flips the gate ON for every Azure SQL DB (all engine-edition-5 tiers), but the live evidence only covers General Purpose and Hyperscale, neither with real secondary replicas.
The PR's own follow-up (#1887) admits Business Critical / Hyperscale-with-HA-replicas / provisioned Hyperscale are unverified — but Business Critical is exactly the tier replica_role exists to serve (it has real AG-style secondary replicas), and it's untested here.
If sys.query_store_replicas / replica_group_id don't bind on some tier, the failure isn't loud. I traced the per-database loop that runs this query (Lite/Services/RemoteCollectorService.DefinitionRunner.cs:135-213, mirrored in Darling/PerformanceMonitor.Darling.Service/DarlingCollectorRunner.cs): a SqlException on one database is caught and logged at Debug level, then that database is silently skipped (RemoteCollectorService.DefinitionRunner.cs:206-213). Only a 100%-failure-across-every-database-in-the-cycle case surfaces a Warning and rethrows.
So on a server with a mix of tiers (e.g. some GP, some Business Critical databases), a bind failure on the untested tier would quietly drop Query Store collection for just those databases, with no operator-visible signal at default log levels — the exact "every database going dark" scenario the removed comment warned about becomes instead a silent partial loss, which is arguably harder to notice.
Given the comment's own framing of the risk, might be worth verifying against a database with actual secondary replicas (Business Critical, or provisioned Hyperscale with HA) before this ships broadly, rather than deferring that check to #1887 after the gate is already on in production for all tiers.
ReviewWhat it does: Flips Parity: No concern — Correctness: The gate logic itself is sound and well-isolated — the LEFT JOIN shape (vs. an INNER JOIN or filter) is unchanged from the existing on-prem 2022+ path and is deliberately not tightened, avoiding the "empty Flagged inline: the gate turns on for all Azure SQL DB tiers (all engine-edition-5 SKUs), but the live-evidence validation described in the PR only covers General Purpose and Hyperscale, neither with actual secondary replicas — Business Critical (which has real AG-style secondaries and is exactly the tier this feature targets) is explicitly filed as unverified in the PR's own follow-up (#1887). I traced the per-database error handling ( Minor nit: the CHANGELOG diff introduces one stray blank line into the Security: No new input handling, external calls, or secrets touched — this is a boolean gate change over an existing, already-parameterized query shape. No missing-index DMV suggestions offered, per instructions. |
Flips Query Store replica attribution ON for Azure SQL Database, on the live evidence the gate's own comment asked for, and corrects the stale reader-ordinal count in the same file.
The decision
QueryStoreCollector.BuildPayloadBody's gate becomes:(was
productVersion >= 16 && !context.Target.IsAzureSqlDb)Azure reports
ProductMajorVersion12 but carries the 2022+ catalog, so the version probe cannot speak for it. This is the same "Azure means newest" rulehasPlanTypeon the line above already uses.Why now: the probe the old comment demanded was run, on both tiers
The #1836 carve-out was about bind SAFETY, not taste: a missing column is not a NULL, it fails the whole SELECT for that database, and Azure collects per database, so a wrong guess would have meant every database going dark rather than one column. The docs still do not settle it. Live runs do, on both tiers, 2026-07-31 UTC, each on
Microsoft SQL Azure (RTM) - 12.0.2000.8, EngineEdition 5:OBJECT_ID('sys.query_store_replicas')COL_LENGTH('sys.query_store_runtime_stats', 'replica_group_id')replica_rolereturnedPrimaryThe Hyperscale run is the decisive one: it composed
BuildPayloadBodyfromorigin/devwithhasReplicaAttribution = trueand the Darling shape (plan XML on), diffed the text against the already-verified 55-column Azure file to confirm the only differences were the three intended ones, and executed it.A per-tier gate was considered and rejected as unbuildable: Hyperscale reports EngineEdition 5, the same as General Purpose, so the collector cannot tell the tiers apart at query-build time. Since both bind identically, it does not need to.
Managed Instance deliberately keeps the pure version gate — #1844's reasoning preserved. MI under-reports PRODUCTVERSION too, but its feature set follows a per-instance update policy rather than being evergreen, so "Azure means 2022+" is not sound there, and MI has no per-database path this could have been verified on.
IsAzureSqlDbis false for MI, so it falls through toproductVersion >= 16and stays off.Read side: verified, zero changes needed
Every Query Store dedup key already partitions on
replica_roleand every aggregate already groups on it, so populated Azure values slot straight in:Lite/Services/LocalDataService.QueryStore.cs— fivePARTITION BY ... execution_type_desc, replica_rolededup windows plusGROUP BY database_name, query_id, plan_id, query_hash, replica_roleDarling/PerformanceMonitor.Darling.Viewer/ViewerDataService.QueryStore.cs— the same shape, twinnedDarling/.../Mcp/DarlingDataReader.cs,Compose/ComposeCompiler.cs, and theTimescaleSupportcorrected-rollup CAGGThe column itself has existed in the Darling store since migration V28, and in Lite's schema since #1546. The reader's ordinal 53 is unchanged.
Lite's own comment already spelled out the case this lands in: "On a standalone/non-AG server every row shares one value (NULL, or 'Primary' on 2025), so the grouping is a no-op and the grid is unchanged." An Azure database goes from one NULL group to one
Primarygroup — still one group, still a no-op, grids render identically. The value simply stops being a lie by omission.#1871, riding along
BuildPayloadBody's XML doc said the two execution paths select "53 reader ordinals"; the real count is 55. The class-level doc andReadRowsAsync(whose highest ordinal is 54) already agreed on 55 — the count was just never updated when #1841 added the interval-identity pair. It matters because that comment is the drift guard between the two paths' column sets, so a wrong number sits in exactly the place a future audit would trust.Swept the repo for other propagations: there are none. Every remaining
53in the codebase is a real reader ordinal (reader.IsDBNull(53),names[53],row[53]), not a column count. One adjacent doc correction is folded in:Row.ReplicaRole's summary named Azure SQL DB as a NULL source, which this PR makes false — it now names Managed Instance instead and records why Azure changed.Test plan
Lite.Tests: 1729 passed, 0 failedDarling.Tests: 3656 passed, 0 failed, 201 skipped (the live-PostgreSQL set, gated on env vars exactly as the per-PR CI job gates them)-t:Rebuild: 0 Warning(s), 0 Error(s)Pins changed in
Lite.Tests/QueryStoreCollectorDefinitionTests.cs:BuildQuery_Azure_GatesReplicaAttributionOff_ButKeepsTheColumn->BuildQuery_Azure_AttributesReplicaRole_ProvenLiveOnBothServiceTiers. This pin is deliberately inverted — it previously asserted the join and column absent. Its comment now carries the evidence (both tiers, both probe values, the 55/55 Hyperscale payload run) and an explicit instruction not to re-flip it without new live evidence, because a bare inversion is indistinguishable from a regression to the next reader.BuildQuery_Azure_AttributesReplicaRole_EvenWhenTheVersionProbeFails— the Azure arm is an OR, not an AND. If attribution depended on the version at all it would never turn on there, so the edition alone has to decide it.BuildPerItemQuery_AzureWithFailedProbe_StillNewColumnsgains the MI pin:replica_roleOFF and nosys.query_store_replicas, beside its existingplan_type = NULLassertion, so MI's carve-out is pinned rather than merely commented.Unchanged and still green: the pre-2022 / 2017 on-prem shapes still exclude the join,
PayloadColumns_MatchSchemaOrder_56Columnsand the 56-column write-order pin are untouched, and the shared-body drift guard (BuildQuery_Azure_AndPerItemWrapper_ShareTheSinglePayloadBody) still proves both builders emit one body.Filed rather than folded in
Nothing in the implementation was deferred — the gate, the comment, the #1871 propagation sweep, the test pins and the MI pin are all in this PR. Two things this change leaves unverified are filed so they do not vanish when #1872 closes:
readonly_reason & 8), which would otherwise be buried by closing Query Store replica attribution on Azure SQL DB: General Purpose probe now passes; needs one Hyperscale probe to lift the gate #1872.Closes #1872
Closes #1871
🤖 Generated with Claude Code