From 76315587d68426db4be7e75586c50376254fdd70 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Wed, 29 Apr 2026 01:41:33 +0400 Subject: [PATCH] feat(sql): emit compact displayQuery preview for wide SELECTs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Populate RecentQuery.displayQuery via compactSelectList from @query-doctor/core 0.8.7 — a display-only preview with the top-level SELECT target list replaced by `...`, e.g. `SELECT ... FROM users WHERE id = $1`. The site renders this in its truncated cells (live queries list, sidebar, detail header, CI run list/detail, project catalog) so ORM-written queries with long target lists stop drowning out the table/filter parts. The algorithm lives in @query-doctor/core so the analyzer service and the in-browser pglite path call the same implementation. Site side: Query-Doctor/Site#2800. RecentQuery.computeDisplayQuery wraps the call in a try/catch around parse() so a parse failure debug-logs and returns undefined; the site then falls back to the raw query. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/sql/recent-query.test.ts | 40 ++++++++++++++++++++++++++++++++++++ src/sql/recent-query.ts | 22 +++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/sql/recent-query.test.ts b/src/sql/recent-query.test.ts index 8bdd6dde..7d2b8156 100644 --- a/src/sql/recent-query.test.ts +++ b/src/sql/recent-query.test.ts @@ -259,3 +259,43 @@ test("analyze sets isSelectQuery=false for DELETE with EXISTS subquery", async ( const rq = await RecentQuery.analyze(data, testHash, 1000); expect(rq.isSelectQuery).toBe(false); }); + +// --- displayQuery via analyze --- + +test("analyze populates displayQuery for wide SELECTs", async () => { + const data = makeRawQuery({ + query: + 'SELECT "u"."id", "u"."email", "u"."first_name", "u"."last_name", "u"."created_at", "u"."updated_at", "u"."stripe_customer_id" FROM "users" "u" WHERE "u"."id" = $1', + }); + const rq = await RecentQuery.analyze(data, testHash, 1000); + // Normalize whitespace because the analyzer prettier-formats the query + // before compacting; the site applies the same normalization on render. + const normalized = rq.displayQuery?.replace(/\s+/g, " ").trim(); + expect(normalized).toBe( + `SELECT ... FROM "users" "u" WHERE "u"."id" = $1;`, + ); +}); + +test("analyze leaves displayQuery undefined for narrow SELECTs", async () => { + const data = makeRawQuery({ query: "SELECT id FROM users WHERE id = $1" }); + const rq = await RecentQuery.analyze(data, testHash, 1000); + expect(rq.displayQuery).toBeUndefined(); +}); + +test("analyze leaves displayQuery undefined for non-SELECTs", async () => { + const data = makeRawQuery({ + query: + "INSERT INTO archive SELECT a, b, c, d, e, f, g, h FROM users WHERE active = false", + }); + const rq = await RecentQuery.analyze(data, testHash, 1000); + expect(rq.displayQuery).toBeUndefined(); +}); + +test("analyze leaves displayQuery undefined for UNION", async () => { + const data = makeRawQuery({ + query: + "SELECT a, b, c, d, e, f, g FROM t UNION SELECT a, b, c, d, e, f, g FROM u", + }); + const rq = await RecentQuery.analyze(data, testHash, 1000); + expect(rq.displayQuery).toBeUndefined(); +}); diff --git a/src/sql/recent-query.ts b/src/sql/recent-query.ts index 877f0cae..39290c0e 100644 --- a/src/sql/recent-query.ts +++ b/src/sql/recent-query.ts @@ -3,6 +3,7 @@ import prettierPluginSql from "prettier-plugin-sql"; import type { SegmentedQueryCache } from "../sync/seen-cache.ts"; import { Analyzer, + compactSelectList, DiscoveredColumnReference, Nudge, PostgresQueryBuilder, @@ -13,6 +14,7 @@ import { } from "@query-doctor/core"; import { parse } from "@libpg-query/parser"; import z from "zod"; +import { log } from "../log.ts"; import type { LiveQueryOptimization } from "../remote/optimization.ts"; /** @@ -24,6 +26,7 @@ export class RecentQuery { private static rewriter = new PssRewriter(); readonly formattedQuery: string; + readonly displayQuery?: string; readonly username: string; readonly query: string; readonly meanTime: number; @@ -52,6 +55,7 @@ export class RecentQuery { this.username = data.username; this.query = data.query; this.formattedQuery = data.formattedQuery; + this.displayQuery = data.displayQuery; this.meanTime = data.meanTime; this.calls = data.calls; this.rows = data.rows; @@ -119,8 +123,9 @@ export class RecentQuery { ); const analysis = await analyzer.analyze(formattedQuery); const query = this.rewriteQuery(analysis.queryWithoutTags); + const displayQuery = await RecentQuery.computeDisplayQuery(query); return new RecentQuery( - { ...data, query, formattedQuery }, + { ...data, query, formattedQuery, displayQuery }, analysis.referencedTables, analysis.indexesToCheck, analysis.tags, @@ -152,6 +157,20 @@ export class RecentQuery { } } + private static async computeDisplayQuery( + query: string, + ): Promise { + try { + return compactSelectList(query, await parse(query)); + } catch (error) { + log.debug( + `displayQuery: parse failed (${(error as Error).message})`, + "display-query", + ); + return undefined; + } + } + static isSelectQuery(data: RawRecentQuery): boolean { return /^\s*select/i.test(data.query); } @@ -180,6 +199,7 @@ export type RawRecentQuery = { username: string; query: string; formattedQuery: string; + displayQuery?: string; meanTime: number; calls: string; rows: string;