diff --git a/Dockerfile b/Dockerfile index b7fa0c7..88cfd91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,6 +90,7 @@ RUN deno compile \ --allow-write \ --allow-env \ --allow-net \ + --allow-sys \ -o /app/analyzer \ src/main.ts diff --git a/deno.json b/deno.json index 4a77419..dbe83cb 100644 --- a/deno.json +++ b/deno.json @@ -42,7 +42,8 @@ "jsondiffpatch": "npm:jsondiffpatch@^0.7.3", "nunjucks": "npm:nunjucks@^3.2.4", "pgsql-deparser": "npm:pgsql-deparser@^17.11.1", - "sql-formatter": "npm:sql-formatter@^15.6.6", + "prettier": "npm:prettier@^3", + "prettier-plugin-sql": "npm:prettier-plugin-sql@^0.19", "sql-highlight": "npm:sql-highlight@^6.1.0", "postgresjs": "https://deno.land/x/postgresjs@v3.4.7/mod.js", "zod": "npm:zod@^4.1.12" diff --git a/deno.lock b/deno.lock index 62bd55b..684e0d2 100644 --- a/deno.lock +++ b/deno.lock @@ -31,7 +31,8 @@ "npm:jsondiffpatch@~0.7.3": "0.7.3", "npm:nunjucks@^3.2.4": "3.2.4_chokidar@4.0.3", "npm:pgsql-deparser@^17.11.1": "17.12.1", - "npm:sql-formatter@^15.6.6": "15.6.6", + "npm:prettier-plugin-sql@0.19": "0.19.2_prettier@3.7.4", + "npm:prettier@3": "3.7.4", "npm:sql-highlight@^6.1.0": "6.1.0", "npm:zod@^4.1.12": "4.1.13" }, @@ -218,7 +219,7 @@ "@protobufjs/path", "@protobufjs/pool", "@protobufjs/utf8", - "@types/node@24.10.1", + "@types/node", "long" ], "scripts": true @@ -368,7 +369,7 @@ "@types/docker-modem@3.0.6": { "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", "dependencies": [ - "@types/node@24.2.0", + "@types/node", "@types/ssh2" ] }, @@ -376,35 +377,32 @@ "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==", "dependencies": [ "@types/docker-modem", - "@types/node@24.2.0", + "@types/node", "@types/ssh2" ] }, "@types/node@24.10.1": { "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dependencies": [ - "undici-types@7.16.0" - ] - }, - "@types/node@24.2.0": { - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", - "dependencies": [ - "undici-types@7.10.0" + "undici-types" ] }, "@types/nunjucks@3.2.6": { "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==" }, + "@types/pegjs@0.10.6": { + "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==" + }, "@types/ssh2-streams@0.1.13": { "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", "dependencies": [ - "@types/node@24.2.0" + "@types/node" ] }, "@types/ssh2@0.5.52": { "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", "dependencies": [ - "@types/node@24.2.0", + "@types/node", "@types/ssh2-streams" ] }, @@ -533,6 +531,9 @@ "before-after-hook@2.2.3": { "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "big-integer@1.6.52": { + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" + }, "bl@4.1.0": { "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dependencies": [ @@ -792,6 +793,10 @@ ], "bin": true }, + "jsox@1.2.125": { + "integrity": "sha512-HIf1uwublnXZsy7p3yHTrhzMzrLO6xKnqXytT9pEil5QxaXi8eyer7Is4luF5hYSV4kD3v03Y32FWoAeVYTghQ==", + "bin": true + }, "lazystream@1.0.1": { "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dependencies": [ @@ -872,6 +877,13 @@ ], "bin": true }, + "node-sql-parser@5.3.13": { + "integrity": "sha512-heyWv3lLjKHpcBDMUSR+R0DohRYZTYq+Ro3hJ4m9Ia8ccdKbL5UijIaWr2L4co+bmmFuvBVZ4v23QW2PqvBFAA==", + "dependencies": [ + "@types/pegjs", + "big-integer" + ] + }, "normalize-path@3.0.0": { "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, @@ -913,6 +925,20 @@ "@pgsql/types" ] }, + "prettier-plugin-sql@0.19.2_prettier@3.7.4": { + "integrity": "sha512-DAu1Jcanpvs32OAOXsqaVXOpPs4nFLVkB3XwzRiZZVNL5/c+XdlNxWFMiMpMhYhmCG5BW3srK8mhikCOv5tPfg==", + "dependencies": [ + "jsox", + "node-sql-parser", + "prettier", + "sql-formatter", + "tslib" + ] + }, + "prettier@3.7.4": { + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "bin": true + }, "process-nextick-args@2.0.1": { "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, @@ -946,7 +972,7 @@ "@protobufjs/path", "@protobufjs/pool", "@protobufjs/utf8", - "@types/node@24.10.1", + "@types/node", "long" ], "scripts": true @@ -1043,8 +1069,8 @@ "split-ca@1.0.1": { "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==" }, - "sql-formatter@15.6.6": { - "integrity": "sha512-bZydXEXhaNDQBr8xYHC3a8thwcaMuTBp0CkKGjwGYDsIB26tnlWeWPwJtSQ0TEwiJcz9iJJON5mFPkx7XroHcg==", + "sql-formatter@15.6.12": { + "integrity": "sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ==", "dependencies": [ "argparse", "nearley" @@ -1188,15 +1214,15 @@ "tmp@0.2.5": { "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "tunnel@0.0.6": { "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" }, "tweetnacl@0.14.5": { "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, - "undici-types@7.10.0": { - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" - }, "undici-types@7.16.0": { "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" }, @@ -1666,7 +1692,8 @@ "npm:jsondiffpatch@~0.7.3", "npm:nunjucks@^3.2.4", "npm:pgsql-deparser@^17.11.1", - "npm:sql-formatter@^15.6.6", + "npm:prettier-plugin-sql@0.19", + "npm:prettier@3", "npm:sql-highlight@^6.1.0", "npm:zod@^4.1.12" ] diff --git a/src/runner.ts b/src/runner.ts index 0fb5b3f..974bc3b 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,5 +1,6 @@ import * as core from "@actions/core"; -import { format } from "sql-formatter"; +import * as prettier from "prettier"; +import prettierPluginSql from "prettier-plugin-sql"; import csv from "fast-csv"; import { Readable } from "node:stream"; import { fingerprint } from "@libpg-query/parser"; @@ -190,8 +191,9 @@ export class Runner { this.seenQueries.add(fingerprintNum); const analyzer = new Analyzer(parse); + const formattedQuery = await this.formatQuery(query); const { indexesToCheck, ansiHighlightedQuery, referencedTables } = - await analyzer.analyze(this.formatQuery(query)); + await analyzer.analyze(formattedQuery); const selectsCatalog = referencedTables.find((table) => table.startsWith("pg_") @@ -220,7 +222,7 @@ export class Runner { kind: "cost_past_threshold", warning: { fingerprint: fingerprintNum, - formattedQuery: this.formatQuery(query), + formattedQuery, baseCost: log.plan.cost, explainPlan: log.plan.json, maxCost: this.maxCost, @@ -274,7 +276,7 @@ export class Runner { kind: "recommendation", recommendation: { fingerprint: fingerprintNum, - formattedQuery: this.formatQuery(query), + formattedQuery, baseCost: out.baseCost, baseExplainPlan: out.baseExplainPlan, optimizedCost: out.finalCost, @@ -298,7 +300,7 @@ export class Runner { kind: "cost_past_threshold", warning: { fingerprint: fingerprintNum, - formattedQuery: this.formatQuery(query), + formattedQuery, baseCost: out.baseCost, optimization: { newCost: out.finalCost, @@ -328,12 +330,17 @@ export class Runner { ); } - private formatQuery(query: string) { - return format(query, { - language: "postgresql", - keywordCase: "lower", - linesBetweenQueries: 2, - }); + private async formatQuery(query: string): Promise { + try { + return await prettier.format(query, { + parser: "sql", + plugins: [prettierPluginSql], + language: "postgresql", + keywordCase: "upper", + }); + } catch { + return query; + } } private printLegend() { diff --git a/src/sql/recent-query.ts b/src/sql/recent-query.ts index 35fddb5..5ad0031 100644 --- a/src/sql/recent-query.ts +++ b/src/sql/recent-query.ts @@ -1,4 +1,5 @@ -// import { format } from "sql-formatter"; +import * as prettier from "prettier"; +import prettierPluginSql from "prettier-plugin-sql"; // deno-lint-ignore no-unused-vars import type { SegmentedQueryCache } from "../sync/seen-cache.ts"; import { @@ -41,7 +42,7 @@ export class RecentQuery { ) { this.username = data.username; this.query = data.query; - this.formattedQuery = data.query; + this.formattedQuery = data.formattedQuery; this.meanTime = data.meanTime; this.calls = data.calls; this.rows = data.rows; @@ -68,8 +69,9 @@ export class RecentQuery { ) { const analyzer = new Analyzer(parse); const analysis = await analyzer.analyze(data.query); + const formattedQuery = await RecentQuery.formatQuery(analysis.queryWithoutTags); return new RecentQuery( - { ...data, query: analysis.queryWithoutTags }, + { ...data, query: analysis.queryWithoutTags, formattedQuery }, analysis.referencedTables, analysis.indexesToCheck, analysis.tags, @@ -79,6 +81,19 @@ export class RecentQuery { ); } + private static async formatQuery(query: string): Promise { + try { + return await prettier.format(query, { + parser: "sql", + plugins: [prettierPluginSql], + language: "postgresql", + keywordCase: "upper", + }); + } catch { + return query; + } + } + static isSelectQuery(data: RawRecentQuery): boolean { return /^select/i.test(data.query); }