Skip to content

Commit cafbb33

Browse files
committed
chore: better logging
1 parent a68e078 commit cafbb33

File tree

7 files changed

+188
-104
lines changed

7 files changed

+188
-104
lines changed

analyzer.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,6 @@ export class Analyzer {
278278
return column.columnName === referencedColumn;
279279
});
280280
});
281-
// if (!table) {
282-
// console.error(
283-
// `Table ${referencedColumn} does not exist in the introspected tables`
284-
// );
285-
// continue;
286-
// }
287281
for (const table of matchingTables) {
288282
allIndexes.push({
289283
schema: table.schemaName,

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@std/assert": "jsr:@std/assert@1",
1111
"@std/collections": "jsr:@std/collections@^1.1.1",
1212
"@std/data-structures": "jsr:@std/data-structures@^1.0.8",
13+
"@std/fmt": "jsr:@std/fmt@^1.0.8",
1314
"dedent": "npm:dedent@^1.6.0",
1415
"fast-csv": "npm:fast-csv@^5.0.2",
1516
"pgsql-deparser": "npm:pgsql-deparser@^17.8.1",

deno.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export function preprocessEncodedJson(jsonString: string): string | undefined {
2+
let isJSONOutput = false;
3+
let i = 0;
4+
for (; i < jsonString.length; i++) {
5+
const char = jsonString[i];
6+
// skipping escaped newlines
7+
if (char === "\\" && jsonString[i + 1] === "n") {
8+
i++;
9+
continue;
10+
} else if (/\s+/.test(char)) {
11+
// probably not incredibly performant
12+
continue;
13+
} else if (char === "{") {
14+
isJSONOutput = true;
15+
break;
16+
}
17+
}
18+
if (!isJSONOutput) {
19+
return;
20+
}
21+
return unescapeEncodedJson(jsonString.slice(i));
22+
}
23+
24+
function unescapeEncodedJson(jsonString: string) {
25+
return (
26+
jsonString
27+
.replace(/\\n/g, "\n")
28+
// there are random control characters in the json lol
29+
// deno-lint-ignore no-control-regex
30+
.replace(/[\u0000-\u001F]+/g, (c) =>
31+
c === "\n" ? "\\n" : c === "\r" ? "\\r" : c === "\t" ? "\\t" : ""
32+
)
33+
);
34+
}

main.ts

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
GithubReporter,
1414
ReportIndexRecommendation,
1515
} from "./reporters/github.ts";
16+
import { preprocessEncodedJson } from "./json.ts";
17+
import { ExplainedLog } from "./pg_log.ts";
1618

1719
function formatQuery(query: string) {
1820
return format(query, {
@@ -63,6 +65,7 @@ async function main() {
6365
const recommendations: ReportIndexRecommendation[] = [];
6466
let allQueries = 0;
6567
let matching = 0;
68+
let skipped = {};
6669
const pg = postgres(postgresUrl);
6770
const stats = new Statistics(pg);
6871
const existingIndexes = await stats.getExistingIndexes();
@@ -91,82 +94,67 @@ async function main() {
9194
if (loglevel !== "LOG" || !queryString.startsWith("plan:")) {
9295
continue;
9396
}
94-
const plan: string = queryString.split("plan:")[1].trim();
95-
let isJSONOutput = false;
96-
let i = 0;
97-
for (; i < plan.length; i++) {
98-
const char = plan[i];
99-
if (char === "\\" && plan[i + 1] === "n") {
100-
i++;
101-
continue;
102-
} else if (/\s+/.test(char)) {
103-
continue;
104-
} else if (char === "{") {
105-
isJSONOutput = true;
106-
break;
107-
}
108-
}
109-
if (!isJSONOutput) {
110-
return;
97+
const planString: string = queryString.split("plan:")[1].trim();
98+
const json = preprocessEncodedJson(planString);
99+
if (!json) {
100+
continue;
111101
}
112-
const json = plan
113-
.slice(i)
114-
.replace(/\\n/g, "\n")
115-
// there are random control characters in the json lol
116-
// deno-lint-ignore no-control-regex
117-
.replace(/[\u0000-\u001F]+/g, (c) =>
118-
c === "\n" ? "\\n" : c === "\r" ? "\\r" : c === "\t" ? "\\t" : ""
119-
);
120-
let parsed: any;
102+
let parsed: ExplainedLog;
121103
try {
122-
parsed = JSON.parse(json);
104+
parsed = new ExplainedLog(json);
123105
} catch (e) {
124106
console.log(e);
125107
break;
126108
}
127109
allQueries++;
128-
const queryFingerprint = await fingerprint(parsed["Query Text"]);
110+
const { query, parameters, plan } = parsed;
111+
const queryFingerprint = await fingerprint(query);
129112
if (
130113
// TODO: we can support inserts/updates too. Just need the right optimization for it.
131-
parsed.Plan["Node Type"] === "ModifyTable" ||
132-
parsed["Query Text"].includes("@qd_introspection")
114+
plan.nodeType === "ModifyTable" ||
115+
query.includes("@qd_introspection")
133116
) {
134117
continue;
135118
}
136119
const fingerprintNum = parseInt(queryFingerprint, 16);
137120
if (seenQueries.has(fingerprintNum)) {
138-
console.log("Skipping duplicate query", fingerprintNum);
121+
if (process.env.DEBUG) {
122+
console.log("Skipping duplicate query", fingerprintNum);
123+
}
139124
continue;
140125
}
141126
seenQueries.add(fingerprintNum);
142-
const query = parsed["Query Text"];
143-
const rawParams = parsed["Query Parameters"];
144-
const params = rawParams ? extractParams(rawParams) : [];
145127
const analyzer = new Analyzer();
146128

147129
const { indexesToCheck, ansiHighlightedQuery, referencedTables } =
148-
await analyzer.analyze(formatQuery(query), params);
130+
await analyzer.analyze(formatQuery(query), parameters);
149131

150132
const selectsCatalog = referencedTables.find((table) =>
151133
table.startsWith("pg_")
152134
);
153135
if (selectsCatalog) {
154-
console.log(
155-
"Skipping query that selects from catalog tables",
156-
selectsCatalog,
157-
fingerprintNum
158-
);
136+
if (process.env.DEBUG) {
137+
console.log(
138+
"Skipping query that selects from catalog tables",
139+
selectsCatalog,
140+
fingerprintNum
141+
);
142+
}
159143
continue;
160144
}
161-
console.log(query);
162145
const indexCandidates = analyzer.deriveIndexes(tables, indexesToCheck);
163146
if (indexCandidates.length > 0) {
164147
await core.group(`query:${fingerprintNum}`, async () => {
165148
console.time(`timing`);
166149
matching++;
167150
printLegend();
168151
console.log(ansiHighlightedQuery);
169-
const out = await optimizer.run(query, params, indexCandidates, tables);
152+
const out = await optimizer.run(
153+
query,
154+
parameters,
155+
indexCandidates,
156+
tables
157+
);
170158
if (out.newIndexes.size > 0) {
171159
const newIndexes = Array.from(out.newIndexes)
172160
.map((n) => out.triedIndexes.get(n)?.definition)
@@ -185,11 +173,7 @@ async function main() {
185173
}
186174
})
187175
.filter((i) => i !== undefined);
188-
console.log(dedent`
189-
Optimized cost from ${out.baseCost} to ${out.finalCost}
190-
Existing indexes: ${Array.from(out.existingIndexes).join(", ")}
191-
New indexes: ${newIndexes.join(", ")}
192-
`);
176+
console.log(`New indexes: ${newIndexes.join(", ")}`);
193177
recommendations.push({
194178
formattedQuery: formatQuery(query),
195179
baseCost: out.baseCost,
@@ -219,27 +203,11 @@ async function main() {
219203
timeElapsed: Date.now() - startDate.getTime(),
220204
},
221205
});
206+
console.log(seenQueries.size);
222207
console.timeEnd("total");
223208
Deno.exit(0);
224209
}
225210

226-
const paramPattern = /\$(\d+)\s*=\s*(?:'([^']*)'|([^,\s]+))/g;
227-
function extractParams(logLine: string) {
228-
const paramsArray = [];
229-
let match;
230-
231-
while ((match = paramPattern.exec(logLine)) !== null) {
232-
const paramValue = match[2] !== undefined ? match[2] : match[3];
233-
// Push the value directly into the array.
234-
// The order is determined by the $1, $2, etc. in the log line.
235-
paramsArray[parseInt(match[1]) - 1] = paramValue;
236-
}
237-
238-
// Filter out any empty slots if parameters were not consecutive (e.g., $1, $3 present, but $2 missing)
239-
// This ensures a dense array without 'empty' items.
240-
return paramsArray.filter((value) => value !== undefined);
241-
}
242-
243211
if (import.meta.main) {
244212
await main();
245213
}

0 commit comments

Comments
 (0)