From 9881aadb16a9ce665fb17c9304d218d7fbe8e6b4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Fri, 3 Apr 2026 08:29:01 +0400 Subject: [PATCH 1/3] fix: compute total from uploadable results instead of log entries The `total` counter was incrementing during log parsing, counting raw entries before deduplication and optimization. This caused the CI comment to show a different query count than the site, which only displays queries with uploadable states (improvements_available, no_improvement_found, error). Compute `total` from the deduplicated, optimized results filtered to uploadable states so the CI comment and site always agree. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/runner.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runner.ts b/src/runner.ts index bc44d192..4450c733 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -114,8 +114,6 @@ export class Runner { error = err; }); - let total = 0; - console.time("total"); const recentQueries: RecentQuery[] = []; for await (const chunk of stream) { @@ -164,7 +162,6 @@ export class Runner { continue; } - total++; const recentQuery = await RecentQuery.fromLogEntry(query, hash); recentQueries.push(recentQuery) } @@ -185,7 +182,7 @@ export class Runner { }); console.log( - `Matched ${this.remote.optimizer.validQueriesProcessed} queries out of ${total}`, + `Matched ${this.remote.optimizer.validQueriesProcessed} queries`, ); const recommendations: ReportIndexRecommendation[] = []; @@ -250,6 +247,9 @@ export class Runner { } } + const uploadableStates = new Set(["improvements_available", "no_improvement_found", "error"]); + const total = allResults.filter((q) => uploadableStates.has(q.optimization.state)).length; + const statistics = deriveIndexStatistics(filteredRecommendations); const timeElapsed = Date.now() - startDate.getTime(); const reportContext: ReportContext = { From 3edd5a4980334cefb8e0a00c05a744060b1ff75d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Fri, 3 Apr 2026 09:58:54 +0400 Subject: [PATCH 2/3] test: add tests for countUploadableQueries Extract UPLOADABLE_STATES and countUploadableQueries as exported functions so they can be tested. Tests verify: - Only improvements_available, no_improvement_found, error are counted - not_supported, timeout, waiting, optimizing are excluded - countUploadableQueries agrees with buildQueries for every state Co-Authored-By: Claude Opus 4.6 (1M context) --- src/runner.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++ src/runner.ts | 9 +++++-- 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/runner.test.ts diff --git a/src/runner.test.ts b/src/runner.test.ts new file mode 100644 index 00000000..0cf8b707 --- /dev/null +++ b/src/runner.test.ts @@ -0,0 +1,64 @@ +import { test, expect, describe } from "vitest"; +import { countUploadableQueries, UPLOADABLE_STATES } from "./runner.ts"; +import { buildQueries } from "./reporters/site-api.ts"; +import type { OptimizedQuery } from "./sql/recent-query.ts"; + +function fakeQuery(state: string): OptimizedQuery { + return { + optimization: { state }, + } as OptimizedQuery; +} + +describe("countUploadableQueries", () => { + test("counts improvements_available, no_improvement_found, and error", () => { + const results = [ + fakeQuery("improvements_available"), + fakeQuery("no_improvement_found"), + fakeQuery("error"), + ]; + expect(countUploadableQueries(results)).toBe(3); + }); + + test("excludes not_supported, timeout, waiting, optimizing", () => { + const results = [ + fakeQuery("not_supported"), + fakeQuery("timeout"), + fakeQuery("waiting"), + fakeQuery("optimizing"), + ]; + expect(countUploadableQueries(results)).toBe(0); + }); + + test("counts only uploadable in a mixed set", () => { + const results = [ + fakeQuery("improvements_available"), + fakeQuery("not_supported"), + fakeQuery("no_improvement_found"), + fakeQuery("timeout"), + fakeQuery("error"), + fakeQuery("waiting"), + ]; + expect(countUploadableQueries(results)).toBe(3); + }); +}); + +describe("UPLOADABLE_STATES matches buildQueries filter", () => { + test("buildQueries keeps exactly the same states as UPLOADABLE_STATES", () => { + const allStates = [ + "improvements_available", + "no_improvement_found", + "error", + "not_supported", + "timeout", + "waiting", + "optimizing", + ]; + + for (const state of allStates) { + const results = [fakeQuery(state)]; + const uploaded = buildQueries(results).length; + const counted = countUploadableQueries(results); + expect(counted, `state "${state}"`).toBe(uploaded); + } + }); +}); diff --git a/src/runner.ts b/src/runner.ts index 4450c733..d73b1d63 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -247,8 +247,7 @@ export class Runner { } } - const uploadableStates = new Set(["improvements_available", "no_improvement_found", "error"]); - const total = allResults.filter((q) => uploadableStates.has(q.optimization.state)).length; + const total = countUploadableQueries(allResults); const statistics = deriveIndexStatistics(filteredRecommendations); const timeElapsed = Date.now() - startDate.getTime(); @@ -277,4 +276,10 @@ export class Runner { } } +export const UPLOADABLE_STATES = new Set(["improvements_available", "no_improvement_found", "error"]); + +export function countUploadableQueries(results: OptimizedQuery[]): number { + return results.filter((q) => UPLOADABLE_STATES.has(q.optimization.state)).length; +} + export type QueryProcessResult = OptimizedQuery; From d007327902483a70d5d04e3523c3a4d783f0706e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Mon, 20 Apr 2026 11:41:52 +0400 Subject: [PATCH 3/3] refactor: delegate analyzed count to buildQueries, rename total to analyzed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop UPLOADABLE_STATES / countUploadableQueries — they duplicated the filter in buildQueries. Use buildQueries(allResults, config).length directly so the CI comment count is derived from the same list we upload to the site. Rename queryStats.total to queryStats.analyzed across the type, the success template, and test fixtures. Restore the `Matched N unique queries out of M log entries` debug log using recentQueries.length (no separate counter). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/reporters/github/github.test.ts | 10 ++--- src/reporters/github/success.md.j2 | 4 +- src/reporters/reporter.ts | 4 +- src/runner.test.ts | 70 ++++++++--------------------- src/runner.ts | 13 ++---- 5 files changed, 31 insertions(+), 70 deletions(-) diff --git a/src/reporters/github/github.test.ts b/src/reporters/github/github.test.ts index 79525bb8..dbf41afb 100644 --- a/src/reporters/github/github.test.ts +++ b/src/reporters/github/github.test.ts @@ -110,7 +110,7 @@ function makeContext(overrides: Partial = {}): ReportContext { statisticsMode: { kind: "fromAssumption", reltuples: 10000 }, recommendations: [], queriesPastThreshold: [], - queryStats: { total: 28, matched: 10, optimized: 2, errored: 0 }, + queryStats: { analyzed: 28, matched: 10, optimized: 2, errored: 0 }, statistics: [], metadata: { logSize: 1000, timeElapsed: 5000 }, ...overrides, @@ -323,18 +323,18 @@ describe("buildViewModel", () => { }); describe("template rendering", () => { - test("renders queryStats.total as the query count", () => { + test("renders queryStats.analyzed as the query count", () => { const ctx = makeContext({ - queryStats: { total: 5, matched: 3, optimized: 1, errored: 0 }, + queryStats: { analyzed: 5, matched: 3, optimized: 1, errored: 0 }, comparison: makeComparison(), }); const output = renderTemplate(ctx); expect(output).toContain("5 queries analyzed"); }); - test("renders queryStats.total in no-comparison mode", () => { + test("renders queryStats.analyzed in no-comparison mode", () => { const ctx = makeContext({ - queryStats: { total: 3, matched: 1, optimized: 0, errored: 0 }, + queryStats: { analyzed: 3, matched: 1, optimized: 0, errored: 0 }, }); const output = renderTemplate(ctx); expect(output).toContain("3 queries analyzed"); diff --git a/src/reporters/github/success.md.j2 b/src/reporters/github/success.md.j2 index 8e2d97c1..0a30f602 100644 --- a/src/reporters/github/success.md.j2 +++ b/src/reporters/github/success.md.j2 @@ -4,10 +4,10 @@ {% endif %} {% if hasComparison %} -{{ queryStats.total | default('?') }} queries analyzed +{{ queryStats.analyzed | default('?') }} queries analyzed {%- if newQueryCount > 0 %} | {{ newQueryCount }} new quer{{ "ies" if newQueryCount != 1 else "y" }}{% endif %} {% else %} -{{ queryStats.total | default('?') }} queries analyzed — no baseline found to compare against. +{{ queryStats.analyzed | default('?') }} queries analyzed — no baseline found to compare against. > **No baseline on `{{ comparisonBranch }}`** — the analyzer cannot detect regressions without a previous run. To establish a baseline, add a `push` trigger for your comparison branch so the analyzer runs on merges to `{{ comparisonBranch }}`. See the [CI integration guide](https://docs.querydoctor.com/guides/ci-integration/#workflow-trigger) for setup instructions. {% endif %} diff --git a/src/reporters/reporter.ts b/src/reporters/reporter.ts index 8e55315d..bebb16d6 100644 --- a/src/reporters/reporter.ts +++ b/src/reporters/reporter.ts @@ -62,8 +62,8 @@ export type ReportMetadata = { declare const s: unique symbol; export interface ReportStatistics { - /** Number of unique, non-filtered queries analyzed */ - total: number; + /** Number of unique queries analyzed and uploaded to the site */ + analyzed: number; /** Number of queries that matched the query pattern */ matched: number; /** Number of queries that had an index recommendation */ diff --git a/src/runner.test.ts b/src/runner.test.ts index 0cf8b707..0ddbe3b5 100644 --- a/src/runner.test.ts +++ b/src/runner.test.ts @@ -1,64 +1,30 @@ import { test, expect, describe } from "vitest"; -import { countUploadableQueries, UPLOADABLE_STATES } from "./runner.ts"; import { buildQueries } from "./reporters/site-api.ts"; import type { OptimizedQuery } from "./sql/recent-query.ts"; -function fakeQuery(state: string): OptimizedQuery { +function fakeQuery(hash: string, state: string): OptimizedQuery { return { + hash, + query: "", + formattedQuery: "", + nudges: [], + tags: [], + tableReferences: [], optimization: { state }, - } as OptimizedQuery; + } as unknown as OptimizedQuery; } -describe("countUploadableQueries", () => { - test("counts improvements_available, no_improvement_found, and error", () => { +describe("queryStats.analyzed source of truth", () => { + test("buildQueries().length counts exactly the queries reported to the site", () => { const results = [ - fakeQuery("improvements_available"), - fakeQuery("no_improvement_found"), - fakeQuery("error"), + fakeQuery("a", "improvements_available"), + fakeQuery("b", "no_improvement_found"), + fakeQuery("c", "error"), + fakeQuery("d", "not_supported"), + fakeQuery("e", "timeout"), + fakeQuery("f", "waiting"), + fakeQuery("g", "optimizing"), ]; - expect(countUploadableQueries(results)).toBe(3); - }); - - test("excludes not_supported, timeout, waiting, optimizing", () => { - const results = [ - fakeQuery("not_supported"), - fakeQuery("timeout"), - fakeQuery("waiting"), - fakeQuery("optimizing"), - ]; - expect(countUploadableQueries(results)).toBe(0); - }); - - test("counts only uploadable in a mixed set", () => { - const results = [ - fakeQuery("improvements_available"), - fakeQuery("not_supported"), - fakeQuery("no_improvement_found"), - fakeQuery("timeout"), - fakeQuery("error"), - fakeQuery("waiting"), - ]; - expect(countUploadableQueries(results)).toBe(3); - }); -}); - -describe("UPLOADABLE_STATES matches buildQueries filter", () => { - test("buildQueries keeps exactly the same states as UPLOADABLE_STATES", () => { - const allStates = [ - "improvements_available", - "no_improvement_found", - "error", - "not_supported", - "timeout", - "waiting", - "optimizing", - ]; - - for (const state of allStates) { - const results = [fakeQuery(state)]; - const uploaded = buildQueries(results).length; - const counted = countUploadableQueries(results); - expect(counted, `state "${state}"`).toBe(uploaded); - } + expect(buildQueries(results).length).toBe(3); }); }); diff --git a/src/runner.ts b/src/runner.ts index d73b1d63..5afbb231 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -22,6 +22,7 @@ import { QueryHash } from "./sql/recent-query.ts"; import type { OptimizedQuery } from "./sql/recent-query.ts"; import { ExportedStats } from "@query-doctor/core"; import { readFile } from "node:fs/promises"; +import { buildQueries } from "./reporters/site-api.ts"; export class Runner { constructor( @@ -182,7 +183,7 @@ export class Runner { }); console.log( - `Matched ${this.remote.optimizer.validQueriesProcessed} queries`, + `Matched ${this.remote.optimizer.validQueriesProcessed} unique queries out of ${recentQueries.length} log entries`, ); const recommendations: ReportIndexRecommendation[] = []; @@ -247,7 +248,7 @@ export class Runner { } } - const total = countUploadableQueries(allResults); + const analyzed = buildQueries(allResults, config).length; const statistics = deriveIndexStatistics(filteredRecommendations); const timeElapsed = Date.now() - startDate.getTime(); @@ -256,7 +257,7 @@ export class Runner { recommendations: filteredRecommendations, queriesPastThreshold: filteredThresholdWarnings, queryStats: Object.freeze({ - total, + analyzed, matched: this.remote.optimizer.validQueriesProcessed, optimized: filteredRecommendations.length, errored: optimizedQueries.filter((q) => q.optimization.state === "error").length, @@ -276,10 +277,4 @@ export class Runner { } } -export const UPLOADABLE_STATES = new Set(["improvements_available", "no_improvement_found", "error"]); - -export function countUploadableQueries(results: OptimizedQuery[]): number { - return results.filter((q) => UPLOADABLE_STATES.has(q.optimization.state)).length; -} - export type QueryProcessResult = OptimizedQuery;