Skip to content
Merged
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
14 changes: 13 additions & 1 deletion src/remote/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,24 @@ export function hookUpApiReporter(api: RpcStub<ServerApi>, remote: Remote): () =
log.error(`Failed to push stats: ${err}`, "api-client");
});
};
// pg_stat_statements retains everything the database ever executed — COPY
// from dumps/seeds, DDL from migrations, VACUUM. None of it is an app query:
// shipping it pollutes the project's query catalog and fires unfixable
// "no CI coverage" alerts (Query-Doctor/Site#3578). Only DML is pushed.
// `undefined` (analysis skipped or failed) is pushed too — fail open rather
// than silently losing real queries. Known gap: core classifies MergeStmt as
// "other", so MERGE statements are dropped until its map learns them.
const isAppQuery = (query: RecentQuery) => query.statementType !== "other";

const onQueriesPolled = (queries: RecentQuery[]) => {
api.pushQuery(JSON.parse(JSON.stringify(queries))).catch((err) => {
const appQueries = queries.filter(isAppQuery);
if (appQueries.length === 0) return;
api.pushQuery(JSON.parse(JSON.stringify(appQueries))).catch((err) => {
log.error(`Failed to push polled queries: ${err}`, "api-client");
});
};
const pushOptimizedQuery = (query: OptimizedQuery) => {
if (!isAppQuery(query)) return;
const q = [query.toJSON()]
api.pushQuery(q).catch((err) => {
log.error(`Failed to push optimized query: ${err}`, "api-client");
Expand Down
32 changes: 32 additions & 0 deletions src/sql/recent-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,38 @@ test("analyze sets isSelectQuery=false for DELETE with EXISTS subquery", async (
expect(rq.isSelectQuery).toBe(false);
});

// --- statementType persistence (push sites drop "other" — see api-client) ---

test("analyze keeps statementType on the instance for DML", async () => {
const data = makeRawQuery({ query: "SELECT * FROM users" });
const rq = await RecentQuery.analyze(data, testHash, testNormalizedHash);
expect(rq.statementType).toBe("select");
});

test("analyze classifies DDL as other", async () => {
const data = makeRawQuery({
query:
'CREATE INDEX "ci_runs_repo_created_at_idx" ON "ci_runs" USING btree ("repo", "created_at" DESC)',
});
const rq = await RecentQuery.analyze(data, testHash, testNormalizedHash);
expect(rq.statementType).toBe("other");
});

test("analyze classifies COPY as other", async () => {
const data = makeRawQuery({
query: "COPY public.project_queries (id, hash) FROM stdin",
});
const rq = await RecentQuery.analyze(data, testHash, testNormalizedHash);
expect(rq.statementType).toBe("other");
});

test("oversized queries skip analysis and carry no statementType", async () => {
const data = makeRawQuery({ query: `SELECT ${"1,".repeat(30_000)} 1` });
const rq = await RecentQuery.analyze(data, testHash, testNormalizedHash);
expect(rq.analysisSkipped).toBe(true);
expect(rq.statementType).toBeUndefined();
});

// --- displayQuery via analyze ---

test("analyze populates displayQuery for wide SELECTs", async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/sql/recent-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export class RecentQuery {
readonly hash: QueryHash,
readonly normalizedHash: QueryHash,
analysisSkipped = false,
statementType?: StatementType,
// Kept on the instance so push sites can drop non-DML (see api-client);
// undefined when analysis was skipped (oversized) or failed.
readonly statementType?: StatementType,
) {
this.username = data.username;
this.query = data.query;
Expand Down
Loading