Skip to content

Fixes #3081 - #3085

Merged
erikdarlingdata merged 14 commits into
devfrom
fix/3081-tsql-convention-guard
Sep 6, 2026
Merged

erikdarlingdata merged 14 commits into
devfrom
fix/3081-tsql-convention-guard

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

CONTRIBUTING.md's T-SQL Style list opens "All T-SQL code must follow" and nothing enforced any of it. TsqlConventionGuardTests enforces four of the nine bullets, records the other five and the uncovered half of a sixth, and the nine pre-existing violations of the covered subset are fixed here so it lands with no waiver list.

Which rules are covered, and which are not

RuleBullets carries one entry per bullet with an explicit disposition, and TheDispositionMap_PartitionsTheBulletsContributingStates compares its keys against the bullets parsed out of CONTRIBUTING.md for equality — a tenth bullet, or a renamed one, reds and forces a scope decision instead of being silently unguarded. Each entry also names the wording its checks were derived from, asserted still present, so the document cannot be edited to say the opposite of what the detector enforces. The two bullets that are split rather than all-or-nothing are named in that map's own summary, and a set-equality assertion holds the naming, so a third split bullet reds instead of leaving the prose quietly incomplete.

Bullet Covered Not covered, and why
Functions (:355) COUNT( → COUNT_BIG(), @@ROWCOUNT → ROWCOUNT_BIG() —
Comments (:354) -- line comments —
Indentation (:350) tabs the four-space depth — continuation lines, CASE arms and the two-space ON rule all indent by amounts the bullet does not enumerate, so a depth check needs the layout rules rather than the character
Data types (:348) the case half the abbreviation half: int where integer is meant stands at 33 sites in 7 files, all inside T-SQL that runs against monitored production servers, two of them sp_executesql parameter declarations that nothing in this repository executes. Rewriting live query strings belongs in a change whose subject is that rewrite
Keywords (:347) — needs enough parsing to tell a keyword from an identifier containing one
Object names (:349) — depends what the column means: nvarchar(128) is right for a wait type and wrong for a database name
Table aliases (:351) — finding the alias slot means knowing where the FROM list ends
Column aliases (:352) — a style shape, not a token
Commas (:353) — a style shape: a leading comma is legal SQL in a legal position

Every uncovered rule has a fixture in TheUncoveredRules_AreViolatedByFixturesThisGuardDoesNotFlag — a statement that genuinely violates it, asserted to be inside the population so its empty answer records something, with the expected finding set empty and a positive control spliced through the same detector so a dead Findings fails the control rather than satisfying the emptiness. The fixture set's rule coverage is compared to the map's uncovered side at set equality.

The abbreviation half is the one exception to "recorded and left alone": its detector lives beside the enforced ones, reads the same population and the same code view, and TheAbbreviationScopeNote_CountsTheSitesItClaims compares its count to the figure the scope note states. A cost written down once goes stale by exactly the mechanism this guard exists to stop, and the direction that matters is downward — when those sites are fixed, the pin says to move the rule to the covered side rather than leaving an unguarded rule behind an expired reason.

The population, and what it is not

A string literal is judged when, with its own SQL comments blanked, it begins with a T-SQL statement keyword and carries a token only T-SQL has. Both halves earn their keep, measured:

  • The marker alone admits prose. ScheduleManager's collector descriptions name sys.dm_* twenty-three times; the two MCP instruction blobs are 40 kB and 57 kB of markdown about SQL Server.
  • The statement opener alone admits the stores' own PostgreSQL and DuckDB SQL, where COUNT_BIG() does not exist and -- is idiomatic. DarlingServerConnector is the clean demonstration: it carries the same -- #2228 comment twice, once in a T-SQL probe (fixed here) and once in a PostgreSQL probe (left alone, correctly).
  • Anchoring the opener at any line start rather than at the literal's start admitted both instruction blobs — one of them contains a line beginning IF.

Scanned: the seven shared PerformanceMonitor.* libraries, all of Darling/, all of Lite/. Measured on this branch: 1,038 files, 35,001 string literals, 140 T-SQL statements across 67 files.

Stated bounds, each with a live instance rather than left implied:

  • SQL assembled across several literals is judged per literal. FactRemediation renders a remediation script one sb.Append per line, so DECLARE @plan_handle varbinary(64); is in and the -- STEP 2 line above it is not — a literal that is only a comment opens no statement.
  • C# string literals only, not standalone .sql. Measured: the retired Full edition's install/ carries 6 COUNT(), 6 -- comments and 75 uppercase data types; upgrades/ carries 3 @@ROWCOUNT; the rigs and CI scripts under tools/ and .github/sql/ carry 9 COUNT() and 101 -- comments. Bringing them in means ~200 waivers or a remediation of a retired SKU's code, and a guard whose waiver list dwarfs its findings has stopped being one.
  • A T-SQL statement carrying none of the markers is not judged. The residue is enumerated, not asserted away — see the next section, which is the most substantive thing in this change.
  • Both test projects are out: their T-SQL is copies, asserted equal to BuildQuery(context).Text, so a violation cannot exist in the copy without existing in the shipped query — and this file's own fixtures deliberately carry every banned shape.

The claim that was wrong, and what testing it found

The first draft of this said "every read of a monitored SQL Server goes through a DMV, a catalog view or a built-in, so nothing T-SQL falls outside the population." That is a universal quantifier, and a universal quantifier names the one counter-example that would break it while a count only tells you to count again. So it was tested rather than stated: enumerate the population's complement — literals that open a statement and carry no marker — and read the members.

The complement is 500-odd literals, most of them prose that happens to start with a keyword and store SQL for the other two dialects. What separates T-SQL from both is the parameter spelling: @name is T-SQL's, $1 is Npgsql's. Filtering on that gives a set small enough to read, and reading it found two things a tally never could:

  1. DarlingXeSessions' Extended Events provisioning DDL was entirely outside the population. CREATE EVENT SESSION [...] ON SERVER ADD EVENT sqlserver.xml_deadlock_report ADD TARGET package0.ring_buffer (...), and its ALTER/DROP siblings, are real T-SQL sent to production servers on every session repair, and carried no marker at all. TsqlMarkers now reaches EVENT SESSION, and two of those statements are population fixtures. They carry no violation of the covered rules — but they were unjudged, which is this guard's whole failure direction.
  2. FactRemediation's cursor fetch, twice — FETCH NEXT FROM plan_cursor INTO @plan_handle;, the one line of that rendered script with no DMV, no catalog view and no built-in in it.

TheTsqlTheMarkerSetCannotSee_IsTheseAndNoOthers holds the remainder at set equality over the complement, with the complement itself floored, so a third cannot arrive unnoticed. Injecting a UPDATE STATISTICS … WITH FULLSCAN line into that script reds it by name.

sqlserver. and package0. went in beside EVENT SESSION and were then removed: measured, every statement carrying either also carries EVENT SESSION, so no fixture could show them mattering and a mutation deleting them stayed green. A marker nothing depends on is a claim nothing holds.

Pre-existing violations: nine, all fixed, no waiver list

File Was Now
DatabaseSizeStatsCollector ×2 SELECT COUNT(*) FROM sys.dm_db_log_info(…) CONVERT(integer, COUNT_BIG(*)) — the StallWaitProbe pattern, keeping the expression's type identical for an integer destination
LocalDataService.FinOps.Recommendations N'SELECT @c = COUNT(*) FROM sys.availability_groups…' CONVERT(integer, COUNT_BIG(*))
DarlingServerConnector -- #2228: … in the T-SQL probe /* … */; the PostgreSQL probe's identical comment is untouched
QueryStoreCollector @sql NVARCHAR(500) nvarchar(500)
FactRemediation DECLARE @plan_handle VARBINARY(64); varbinary(64)
LocalDataService.FinOps.Recommendations AS NVARCHAR(128), AS INT nvarchar(128), integer
DatabaseSizeStatsCollector, LocalDataService.FinOps.ServerProperties nvarchar(MAX) nvarchar(max)

None of these sites is verbatim-pinned by a parity test, checked before editing. The covered subset then measures zero, so there is no waiver mechanism at all — a waiver mechanism nobody needs is a mechanism the next exception takes for granted.

Population floors

Derived rather than counted, since a count floor goes stale by the same mechanism:

  • files scanned > 0, literals extracted > 0 and statements read > 0, each a separate assertion from violations == 0.
  • Per tree, spelled identically for all three: each of the shared libraries, Darling/ and Lite/ must contribute at least one T-SQL statement, and the tree loop must have run for every tree. A union floored only in total passes while one tree is dark.
  • Per anchor file: each tree names a file whose T-SQL must stay visible — StallWaitProbe.cs (where Fixes #2880 #3078's violation landed) and the two non-shared files this change fixed. A glob reaching part of a tree, or a marker set going blind on one file's shape, now fails by name.
  • Per collector, derived from CollectorCatalog, which is the authority on engine (TargetEngine defaults to SqlServer; PostgresCollectorDefinitionBase seals it to PostgreSql). Every SQL Server definition's source must contribute a T-SQL statement; no PostgreSQL definition's may — while still being required to contain a statement the walk could see, which is the load-bearing half of the negative. Guessing from filenames would be wrong: PgPlanFetcher is named for the Postgres store and its SQL is T-SQL, fetched from a monitored SQL Server. The one definition carrying no SQL at all (pg_cpu_utilization, ingested from Performance Insights) is named at set equality and its entry is corroborated against the source's own wording.

Both SKUs

The scan reads the shared libraries and both app trees — #3078's violation landed in PerformanceMonitor.Collectors, which both products build on, and Lite's RemoteCollectorService and LocalDataService.FinOps.* send T-SQL of their own. Each tree is floored separately and spelled identically, because an arm written as a special case is how the next tree inherits the sibling bug #3067 was filed for.

One class rather than two: the darling filter reaches Lite/**/*.cs and core reaches every shared library, so the "Run Darling tests" step fires on a change to any tree this reads, and darling-tree-guards runs the whole suite on the arm where it did not.

What holds that reachability, stated precisely. CrossAppGuardCiGateTests requires every cross-app read to be filter-reachable, and CONTRIBUTING.md's Writing a Test That Reads the Other SKU's Source — added by #3084, merged into dev while this was open — names the three spellings it can see. A bare root iterated out of a string[] field is not one of them; it is the "segment array declared in another member" case that section calls silent by construction, and the bare "Lite" in ScannedTrees is exactly it. Verified: before the anchors existed, pointing a root at Lite.Tests changed nothing in that guard. The anchors are repo-rooted path literals at the site that reads them, which is the first of the three, so Lite/Services/LocalDataService.FinOps.Recommendations.cs is now in its found set; with them in place the same injection reds by name — re-run against #3084's rewritten matcher after merging dev in. (The injected path has to exist on disk: that guard drops unresolvable references, and the first attempt at this red-proof used a filename that was not there and proved nothing.)

Red-proof evidence

68 mutation runs, applied to a committed tree, each built (build success asserted separately from running, so a stale dll cannot print a clean result), run, restored with an absolute pathspec, and the restore verified by git diff --exit-code HEAD on that path plus a token check — with the baseline run last, reporting Failed: 0, Not Run: 0 every time. 59 killed on the run. The count is derived by enumerating the mutation specs, not added up.

One never applied (a bad anchor; re-specified and killed). Of the eight survivals:

  • three were pins a fixture could not distinguish — fixed, re-run, killed (below);
  • one showed a marker nothing depended on — sqlserver./package0., deleted rather than given a fixture it could not have;
  • one was a malformed spec whose second substitution was a no-op — re-specified and killed;
  • three are the same unkillable shape: relaxing an assertion's own comparison operator (offenders.Count >= 0 twice, anchorStatements >= 0 once). No other test catches that by construction — it is deletion of the pin, not a defect in it, and the evidence that matters for those is the eight product-file and anchor-file mutations below, which exercise the whole path from a file on disk to a named offender with the assertions intact.

Against the real tree, all naming NoTsqlStatementViolatesACoveredConvention:

Mutation Assertion that failed
re-introduce COUNT(*) in DatabaseSizeStatsCollector offenders, [COUNT_BIG-not-COUNT] at the site
re-introduce the -- in DarlingServerConnector offenders, [block-comments-not-double-dash]
re-introduce NVARCHAR(500) / nvarchar(MAX) offenders, [data-types-lowercase]
inject @@ROWCOUNT into LatchStatsCollector offenders, [ROWCOUNT_BIG-not-@@ROWCOUNT]
inject a tab into LatchStatsCollector offenders, [spaces-not-tabs]
point the Lite tree at a tree with no T-SQL the per-tree floor, by name
point the Lite / shared anchor at a file with no T-SQL the anchor assertion, naming the file
SourceFiles yields nothing files scanned > 0
IsTsqlStatement always false the per-tree floor — and four other tests
stop excluding Darling.Tests, so this file's own fixtures are scanned offenders, naming this file

Against the residue pin and the walk: dropping EVENT SESSION (the XE fixtures go unrecognised), inverting the named-parameter tell, injecting a third unmarked T-SQL statement into FactRemediation, marking its fetch so it leaves the residue, and dropping the ]] escape arm from the quoted-identifier walk — the last of these was a finding from the review of this PR, and [a]]INT] is the fixture that holds it.

Against the detector and the discriminator, each killing the fixture that names it: neutering the @@ROWCOUNT check, neutering the tab check, letting the tab check see string values, reverting the XQuery-argument window, de-nesting block comments, mis-handling the '' escape, adding table to the type vocabulary, dropping the FOR XML / AT TIME ZONE collision blanking, keeping quoted identifiers, unanchoring the (max) length spec, removing the COUNT( lookbehind, making the case check case-blind, emptying Findings, replacing the span-kind -- test with a bare text search, anchoring the statement opener at any line start, dropping either half of the population test, and reading markers without blanking comments.

Against the document and the map: renaming a bullet heading, removing a bullet, rewriting a required phrase, adding a covered rule id with no bullet, moving the abbreviation rule to the covered side without a check, splitting a third bullet while keeping both rule-id unions identical (which only the split-pair assertion can see), narrowing the marker set until collectors go blind (which names them), widening it into PostgreSQL vocabulary (which names PgBlockingCollector), falsifying either field of the no-SQL entry, bumping either figure in the scope note, making that figure unparseable, neutering the abbreviation detector, and fixing one int site without moving the rule.

Three mutations that survived, and what came of each:

  • The nested-block-comment fixture put its -- before the inner close, where a nesting and a non-nesting walk both read it as comment — it recorded nothing. Moved after the inner close; re-killed.
  • Removing the type anchor from the (max) length spec changed no answer, because no fixture had (MAX) after a non-type identifier. MAX(MAX) — legal T-SQL, since MAX is not reserved — is textually IDENT ( max ); re-killed.
  • Removing the COUNT( lookbehind changed no answer: COUNT_BIG( and ROWCOUNT_BIG() are rescued by the underscore that follows COUNT, and @@ROWCOUNT is never followed by (. The lookbehind's whole job is an identifier ending in Count that is then called; dbo.RowCount(…) is that shape; re-killed.

A fourth, found by mutating the document rather than by reading the list: the Functions bullet asked for the phrase COUNT_BIG(), and rewriting the document's COUNT_BIG() to COUNTBIG() stayed green, because ROWCOUNT_BIG() two words later still contains COUNT_BIG() as a substring. The phrases now carry their backticks, and EveryRequiredPhrase_IsLoadBearingOnItsOwn deletes each one from the real bullet text in turn and requires the check to red.

Two further red-proofs confirm the harnesses read the edited file rather than a stale copy: adding a line-prefix comment filter makes CommentFilterAdoptionTests name TsqlConventionGuardTests.cs, and pointing the Lite anchor at a real Lite.Tests path makes CrossAppGuardCiGateTests name it.

What was verified locally, and what was not

No query in this diff was executed against a SQL Server, of any version, anywhere. Nine hand-edits to T-SQL that runs against monitored production instances, and the verification for all nine is inspection: mine, and the review bot's independently. Four are keyword case, which T-SQL is insensitive to; one turns a -- comment into a /* */ comment; three wrap COUNT_BIG in CONVERT(integer, …) so the expression's type is unchanged at an integer destination; one changes AS INT to AS integer in a CAST. That is the right level of confidence for changes of this shape, but it is inspection and not execution, and nothing below should be read as covering it — the test suites exercise the guard, not the queries.

Darling.Tests and Lite.Tests are net10.0-windows and cannot run on macOS. The eight tests here, CrossAppGuardCiGateTests (12), and CommentFilterAdoptionTests + DocCommentHygieneTests + FleetIdentifierScrubTests (46) were compiled from the actual .cs files into throwaway net10.0 xunit v3 harnesses staged in the gitignored project bin/ and run — Failed: 0, Not Run: 0 on all three, re-run after merging dev in so the 12 are #3084's rewritten matcher rather than the one this branch started from. Both test projects and the four edited product projects compile with -p:EnableWindowsTargeting=true.

CI on b098a76c4: 8/8 green. build 8m28s ran Darling.Tests (Total: 7814, Failed: 0, Not Run: 0) and Lite.Tests (Total: 3429, Failed: 0, Not Run: 0); Darling PostgreSQL tests 4m12s ran the Darling suite against a live store (Failed: 0, Skipped: 6, Not Run: 0). TsqlConventionGuardTests appears in neither the FAIL nor the SKIP list in either job, and Not Run: 0 means nothing was discovered-but-unrun — so the tests here executed rather than merely compiled. The suite totals are not read as a delta: dev moved during this PR (#3084 merged and is merged in).

Windows CI and the nightly are the arbiters for the suites; neither runs a query from this diff against a SQL Server, per the note that opens this section.

CONTRIBUTING.md's T-SQL Style list opens "All T-SQL code must follow" and
nothing enforced any of it: PR #3078 introduced COUNT(*) and
COUNT(DISTINCT ...) into a new collector query and passed the Linux build,
the PostgreSQL tests, all four whole-tree guards, the command-deadline
family, review and verify.

TsqlConventionGuardTests scans string literals that OPEN a T-SQL statement
across the shared libraries and both SKUs' app trees, and enforces four of
the nine bullets: COUNT_BIG() over COUNT(), ROWCOUNT_BIG() over @@rowcount,
block comments over --, no tabs, and lowercase data types. The other five
bullets, and the abbreviation half of the data-type bullet, are recorded
with their reasons and carry a fixture each whose expected answer is empty.

The nine pre-existing violations of the covered subset are fixed here, so
the guard lands with no waiver list: two COUNT(*) in DatabaseSizeStatsCollector
and one inside sp_executesql in Lite's FinOps recommendations, one --
comment in DarlingServerConnector's T-SQL probe (its PostgreSQL twin keeps
its own, correctly), and NVARCHAR(500), VARBINARY(64), NVARCHAR(128) and two
nvarchar(MAX) spellings.
The Functions bullet's phrase list asked for COUNT_BIG(), and rewriting
CONTRIBUTING.md's COUNT_BIG() to COUNTBIG() left the check green: two words
later ROWCOUNT_BIG() still contains COUNT_BIG() as a substring, so the
Contains was satisfied by the wrong token. Found by mutating the document
rather than by reading the list.

The phrases now carry the backticks the document writes them with, and
EveryRequiredPhrase_IsLoadBearingOnItsOwn deletes each one from the real
bullet text in turn and requires the check to red - so a phrase absorbed by
a longer neighbour cannot pass again.
Two claims had no case that could distinguish them, and a mutation of each
survived.

The nested-block-comment fixture put its -- BEFORE the inner close, where a
nesting and a non-nesting walk both read it as comment. It now sits after the
inner close and before the outer one, which only the nesting walk keeps
inside a comment.

The MAX length spec's requirement that the name in front of it be a data type
changed no answer on any fixture, because none had (MAX) after a non-type
identifier. MAX(MAX) - legal T-SQL, since MAX is not reserved and a column may
be named it - is textually IDENT ( max ) and reds without the anchor.
Removing the lookbehind from the COUNT( pattern changed no answer on any
fixture. COUNT_BIG( and ROWCOUNT_BIG() are rescued by the underscore that
follows COUNT rather than by what precedes it, and @@rowcount is never
followed by an open parenthesis - so the lookbehind's whole job is an
identifier that ends in Count and is then called. dbo.RowCount(...) is that
shape.
…lity

Two things, one change.

A per-tree floor is satisfied by any file in the tree, so each group now
names an anchor whose T-SQL must stay visible: the file #3078's violation
landed in, and the two non-shared files this change fixed. A glob that
reaches part of a tree, or a marker set that goes blind on one file's shape,
now fails by name.

And the anchors are separator-bearing path literals, which is the first arm
of CrossAppGuardCiGateTests' C# matcher. The bare "Lite" in ScannedTrees is
the collection-declared-in-one-member shape that matcher records as
invisible, so before the anchors existed this file's read of Lite was not in
its found set at all - pointing a root at Lite.Tests, which the darling
filter cannot reach, changed nothing. With the anchors it reds.
CrossAppGuardCiGateTests drops references that do not resolve on disk, so
the first attempt at proving the anchor reaches its found set used a
filename that was not there and reported a pass that meant nothing.
The Data types bullet's abbreviation half is left uncovered on a cost - 33
int-for-integer sites across 7 files - and a cost written down once goes
stale by exactly the mechanism this guard exists to stop.

The detector for the rule now lives beside the enforced ones, reads the same
population and the same code view, and its count is compared against the
figure the scope note states. The direction that matters is downward: when
those sites are fixed the pin says to move the rule to the covered side,
rather than leaving an unguarded rule behind a reason that has expired.
"twenty-three times" was 21, which is what an unpinned numeral in prose
does. That one and the command-deadline family's test count are gone; the
figure they were decorating was never the point.

The .sql-tree measurement stays because it is the evidence for a scope
decision, but it now says it was taken once and not re-taken, so nobody
reads it as a live figure. The only live count left is the abbreviation
cost, and a pin holds that one.
The token checks read single-quoted spans other than XQuery arguments, so a
string VALUE containing a banned token would be reported as the token. There
is none, and the direction is a spurious red on correct SQL rather than a
hidden violation - but the alternative is missing every violation inside
dynamic SQL, and one sp_executesql body held one of the nine fixed here.
Worth saying out loud rather than leaving to be rediscovered.
An audit of every numeral in the file found three more that were simply
wrong: "three bullets are split" (two are), "six files carry a -- inside a
block comment" (five do), and "four N'' literals inside one sp_executesql"
(three). Each was decorating a point the numeral was not making.

The split bullets are now named rather than counted, and a set-equality
assertion holds the naming, so a third one reds instead of leaving the prose
incomplete. The rest are named instances or structural facts.
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown

Reviewed the diff (6 production files + the new TsqlConventionGuardTests.cs) against CONTRIBUTING.md's T-SQL Style section, Lite/Darling parity, and correctness/security/perf.

Production fixes (DatabaseSizeStatsCollector, QueryStoreCollector, FactRemediation, DarlingServerConnector, LocalDataService.FinOps.*): all nine edits are the keyword-case / COUNT(*) → CONVERT(integer, COUNT_BIG(*)) / -- → /* */ changes described in the PR body, and each is semantics-preserving on inspection — COUNT_BIG(*) converted to integer matches the existing int-typed destinations (@count int, @c int OUTPUT), keyword casing is insensitive in T-SQL, and the comment-style change is a no-op. OPTION(RECOMPILE) and column_name = expression aliasing are untouched and still correctly in place around the edited lines (e.g. vlf_count = in DatabaseSizeStatsCollector). No parity drift: the two Lite-only and one Darling-only edits are engine-connector-specific code that has no counterpart by design, and the three shared-library fixes (FactRemediation, DatabaseSizeStatsCollector, QueryStoreCollector) are single-source, consumed by both apps.

New test file (Darling/Darling.Tests/TsqlConventionGuardTests.cs, 1502 lines): read in full. The lexer (SpanKinds), the T-SQL population discriminator (StatementOpener + TsqlMarkers), and the five enforced checks (COUNT_BIG, ROWCOUNT_BIG, block-comments, no-tabs, lowercase-types) all look correct against their stated bounds and the fixture set. Verified independently:

  • The RuleBullets map's headings and Phrases all match CONTRIBUTING.md:347-355 verbatim today.
  • The claimed CI reachability (darling path filter includes Lite/**/*.cs via .github/workflows/build.yml:191, added for PR CI skips Lite.Tests when only Darling changes, so cross-app parity guards do not gate merges #2839) actually holds — a Lite-only .cs change does trigger the Darling test job, so the single-class-covers-both-trees design in the docstring is accurate, not just asserted.
  • The regex ordering rationale for TypeName/AbbreviatedTypeUse (backtracking already handles int/integer and date/datetime* prefix collisions regardless of alternation order) checks out.

One low-severity, non-blocking observation: SpanKinds' bracketed-identifier handling (around line 1228) treats the first ] as the closing delimiter and doesn't special-case a doubled ]] escape for a literal ] inside a quoted identifier (e.g. [a]]b]). Nothing in the current corpus hits this — the ]]-producing helpers in FactRemediation.cs/FactRiskDisclosure.cs/IndexCleanupAnalyzer.cs build the bracket via separate short string literals that never open a T-SQL statement, so they're outside the population — but it's a latent gap if a future literal ever embeds an escaped bracket directly. Not worth blocking on.

No correctness bugs, security issues, or Lite/Darling parity drift found. No missing-index-DMV suggestions to make.

PR #3084 landed while this was open and wrote the three spellings
CrossAppGuardCiGateTests can see into CONTRIBUTING.md, naming a segment
array declared in another member as silent by construction. That is the
shape this file's root list has, and the anchors are the first of the three
- so the claim now points at the document rather than at the guard's own
doc comment, and the red-proof was re-run against the merged matcher.
Comment thread Darling/Darling.Tests/TsqlConventionGuardTests.cs
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown

Reviewed. This is a well-constructed, exhaustively self-tested PR — the analysis below focuses on what could still be wrong despite the 54-mutation red-proof already documented.

Production T-SQL fixes (7 files, small diffs): all semantics-preserving and consistent with CONTRIBUTING.md.

  • COUNT(*) → CONVERT(integer, COUNT_BIG(*)) in DatabaseSizeStatsCollector (×2) and LocalDataService.FinOps.Recommendations (inside an sp_executesql body) — same expression type at an integer destination, verified.
  • DarlingServerConnector's T-SQL -- comment → /* */; confirmed the identical comment in the neighboring PostgreSQL probe (PostgresDetectionQueryText) was correctly left alone, since -- is idiomatic there.
  • Case-only fixes (NVARCHAR→nvarchar, VARBINARY→varbinary, nvarchar(MAX)→nvarchar(max)) in QueryStoreCollector, FactRemediation, LocalDataService.FinOps.ServerProperties — no behavior change, T-SQL is case-insensitive.

Lite/Darling parity: checked for a Darling counterpart to each touched Lite-only file (LocalDataService.FinOps.Recommendations.cs, LocalDataService.FinOps.ServerProperties.cs). Darling's equivalent (ViewerDataService.FinOps.Recommendations.cs) queries the PostgreSQL store instead of SQL Server directly, so its COUNT(*) calls are correctly idiomatic Postgres, not a missed T-SQL fix. No drift found. The two shared-library fixes (DatabaseSizeStatsCollector, QueryStoreCollector, FactRemediation) apply to both apps automatically since they live in shared projects.

The new guard itself (TsqlConventionGuardTests.cs, ~1530 lines): read in full. Traced the span-classification walk (line/block comment nesting, string-literal '' escaping, XQuery-argument blanking, quoted-identifier handling), the marker/statement-opener population test, and the cross-app anchor mechanism the PR body claims closes CrossAppGuardCiGateTests reachability for Lite/Services/LocalDataService.FinOps.Recommendations.cs. Verified against CrossAppGuardCiGateTests.CSharpPaths: the anchor is a bare quoted "Lite/..." path literal, which the first regex arm ("(root[/\\][^"]+)") does pick up, so the claim holds. Also verified CollectorCatalog, CollectorTargetEngine, PostgresCollectorDefinitionBase, and the pg_cpu_utilization no-SQL entry all match the current source.

Left one inline comment on a minor, non-blocking gap in the quoted-identifier ([...]) span walk (doesn't handle T-SQL's ]] bracket-escape the way the string-literal walk handles ''). Not currently live in the scanned corpus and not a production T-SQL correctness issue — just a documentation/fixture gap relative to this file's otherwise very thorough treatment of stated bounds.

No security, missing-index-DMV, or performance concerns — this PR only adds a test file and fixes casing/function-name violations in existing collector SQL.

The summary claimed every read of a monitored SQL Server goes through a DMV,
a catalog view or a built-in, so nothing T-SQL could fall outside the
population. That is a universal quantifier, and testing it instead of
stating it found two counter-examples in the population's complement.

One mattered: DarlingXeSessions' provisioning DDL - CREATE/ALTER/DROP EVENT
SESSION with sqlserver.* events and package0.ring_buffer targets - is real
T-SQL sent to production servers on every session repair, and carried no
marker at all. TsqlMarkers now reaches EVENT SESSION, sqlserver. and
package0., and two of those statements are population fixtures. They carry
no violation of the covered rules.

What remains is FactRemediation's cursor fetch, twice, and
TheTsqlTheMarkerSetCannotSee_IsTheseAndNoOthers holds it at set equality
over the complement so a third cannot arrive unnoticed. The complement is
500-odd literals; the discriminator that makes it readable is the parameter
spelling, @name being the one thing neither store dialect writes.

Also closes the ]] escape in the quoted-identifier walk, which the review of
this change flagged as the asymmetry with the '' arm beside it.
sqlserver. and package0. went in beside EVENT SESSION. Measured: zero
statements in the corpus carry either without also carrying EVENT SESSION,
so no fixture could show them mattering and a mutation deleting them stayed
green. A marker nothing depends on is a claim nothing holds. EVENT SESSION
is the one that earns its line, and it is red-proofed.
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown

Reviewed. This is a test-infrastructure PR (new TsqlConventionGuardTests.cs, ~1650 lines) plus six one-line style-only production fixes. Summary of what I checked:

Production diffs (6 files) — all six are pure case/spelling fixes with identical semantics:

  • DarlingServerConnector.cs: -- → /* */ comment, no behavior change.
  • LocalDataService.FinOps.Recommendations.cs: COUNT(*) → CONVERT(integer, COUNT_BIG(*)) inside the sp_executesql body — verified the @c/@count OUTPUT param stays int, so the conversion is a no-op at the boundary. CAST(... AS NVARCHAR(128))/AS INT → lowercase; the sp_executesql parameter spec's own @c int OUTPUT is left alone (abbreviation rule is intentionally out of scope, explained in the test's disposition map).
  • LocalDataService.FinOps.ServerProperties.cs, QueryStoreCollector.cs: nvarchar(MAX)/NVARCHAR(500) → lowercase only.
  • FactRemediation.cs: VARBINARY(64) → lowercase, in a StringBuilder-rendered remediation script (not executed by the app itself).
  • DatabaseSizeStatsCollector.cs: two COUNT(*) → CONVERT(integer, COUNT_BIG(*)) for vlf_count; confirmed the schema (CollectorColumn("vlf_count", CollectorColumnType.Integer), vlf_count int NULL) matches the converted-back type, so no downstream type mismatch.

Test file — read the whole thing against the working tree (not just the per-commit patches). The span-classifier (SpanKinds) correctly nests block comments, handles ''-escaped string quotes and ]]-escaped bracket identifiers (the latter fixed mid-PR per the commit history, and I confirmed the fix is present in the final file). The CountCall/RowcountGlobal/TypeName regexes and their lookarounds check out against the fixtures given (e.g., COUNT_BIG(/ROWCOUNT_BIG() correctly excluded via the trailing-underscore effect, dbo.RowCount( correctly excluded via the leading-letter lookbehind, MAX(MAX) vs nvarchar(max) correctly disambiguated via the type-name anchor). Cross-checked RuleBullets' required phrases against the live CONTRIBUTING.md T-SQL Style bullets (lines 347–355) — all phrases are present verbatim, so TheDispositionMap_PartitionsTheBulletsContributingStates isn't checked-in-broken.

Lite/Darling parity: the guard lives only in Darling.Tests but scans both Darling/ and Lite/ trees (plus shared libraries). No parity drift here — this is a single cross-app test class by design, and the PR leans on the pre-existing CrossAppGuardCiGateTests (Lite.Tests) to keep that reachable from CI's path filters. I didn't re-verify CrossAppGuardCiGateTests itself since it's unmodified by this PR, but the anchor-file mechanism added here (ScannedTrees now carries a repo-rooted anchor path per tree) is exactly the shape that guard's matcher requires, per the PR description.

No correctness, security, or performance issues found. Nothing here touches missing-index DMVs. This is a well-scoped, unusually heavily self-tested change (68 mutation runs cited); I have no findings beyond what the PR description already documents as known limitations (e.g., the abbreviation half of the Data types rule, the 500-literal marker residue).

@erikdarlingdata
erikdarlingdata merged commit c05d029 into dev Sep 6, 2026
11 of 12 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3081-tsql-convention-guard branch September 6, 2026 07:57
erikdarlingdata added a commit that referenced this pull request Sep 6, 2026
* Rewrite the abbreviated `int` data types to `integer` and bring the rule in scope

CONTRIBUTING.md's Data types bullet states two properties. #3085 enforced the case
half and left the abbreviation half uncovered on a remediation cost, with the cost
derived and pinned rather than written down.

The 33 sites across 7 files are rewritten, so UnabbreviatedTypes moves to the Data
types bullet's Covered side, Findings emits it, it gains hazard fixtures and the
near-miss benign fixtures that hold its two anchors, and the cost pin goes with the
cost. Data types stops being a split bullet, so the split-pair assertion names only
Indentation.

Lite.Tests' memory-collector parity pin asserts the Azure variant's
`CONVERT(int, NULL)` verbatim and moves with the query it pins.

* Assert the abbreviation ratchet at its bullet, in both directions

The set equalities in TheDispositionMap_PartitionsTheBulletsContributingStates are
over the flattened union of every bullet's Covered and Uncovered arrays, so they
cannot see which bullet a rule sits under: UnabbreviatedTypes could move to the
Functions bullet and all of them would still hold, while the map claimed this guard
enforces a sentence CONTRIBUTING.md does not have it enforcing.

Naming both sides at the Data types bullet closes that, and it is also what stops
the rule drifting back to Uncovered while Findings still emits it.

* Keep only the ratchet assertion that can fail

Two DoesNotContain lines guarding the Uncovered direction sat after the flattened
set equalities and the disjointness assertion, and every state that would trip them
trips one of those first - so neither could ever be the assertion that reds. They
read as load-bearing while nothing held them.

The bullet-level Covered membership stays, because reparenting the rule to another
bullet is the one drift the unions cannot see, and the comment now names what holds
the other direction so the decorative form does not come back.

* Drop a wrong numeral from a fixture comment

A hazard fixture's comment said nine of the corpus's spellings sat in DDL type
positions. Seven did, and nothing held the figure now that the derived cost pin is
gone - so the comment states the shape instead: a temp table's and a table
variable's column types were both among them, which is what a check anchored only
on CONVERT( would have missed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant