Public: anyone can read this; the crew can change it.

Indexes no prepared query uses are dropped, with the list and the bytes saved in the pull #12023

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/12011-indexes-no-prepared-query-uses-are-dropp → main 2 commits fork

  • Merged. mason/12011-indexes-no-prepared-query-uses-are-dropp was merged into main as f53d2cf.

Review

Sign in to review this change.
5 files changed +550 −0
scripts/index-use.ts added +386
@@ -0,0 +1,386 @@
1 /**
2 * Index use (#12011): which index each statement the forge prepares walks.
3 *
4 * bun scripts/index-use.ts every index, with the statements that use it
5 * bun scripts/index-use.ts --unused only the indexes nothing uses and nothing keeps
6 * bun scripts/index-use.ts --bytes …and what each held on a seeded database
7 * bun scripts/index-use.ts --log <file> add the statements a test run collected
8 *
9 * The statements come from the source: every string or template literal
10 * under `src/` (migrations aside) that reads as SQL, with `${…}` standing
11 * in as a bound parameter, plus the statements inside each trigger. A
12 * test run adds what it really prepared when `FORGE_SQL_LOG=<file>` is set
13 * (`test/helpers/preload.ts` appends each distinct text):
14 *
15 * FORGE_SQL_LOG=$TMPDIR/sql.log bun test test/a.test.ts …
16 * bun scripts/index-use.ts --log $TMPDIR/sql.log
17 *
18 * Each is planned with EXPLAIN QUERY PLAN on a freshly migrated database.
19 * The forge never runs ANALYZE, so production plans without statistics too,
20 * and the plan seen here is the plan it gets.
21 *
22 * An index is never offered for dropping when it is UNIQUE (a constraint),
23 * partial (a bound value can satisfy its WHERE at run time, which a plan
24 * without values cannot show), the lookup a foreign key's checks and
25 * cascades walk (those plans are not in EXPLAIN), or named by any file in
26 * `src/` or `test/` outside the migrations (a test of a plan, an INDEXED BY).
27 */
28
29 import type { Database } from "bun:sqlite";
30 import { appendFileSync, readFileSync } from "node:fs";
31 import { join } from "node:path";
32
33 /** What makes a literal SQL: its first word. */
34 const SQL_START = /^(?:SELECT|WITH|INSERT|UPDATE|DELETE|REPLACE)\b/i;
35
36 /** Where a `${…}` stood in a template literal, before a stand-in is chosen. */
37 const HOLE = "\u0000";
38
39 /**
40 * The string and template literals of one TypeScript file, with literals
41 * joined by `+` read as one, and each `${…}` left as {@link HOLE}.
42 */
43 export function literals(text: string): string[] {
44 const found: { start: number; end: number; value: string }[] = [];
45 let i = 0;
46 // The last significant character, to tell a regular expression from a division.
47 let last = "";
48 const n = text.length;
49
50 const quoted = (q: string): string => {
51 let out = "";
52 i++;
53 while (i < n && text[i] !== q) {
54 if (text[i] === "\\") {
55 out += unescape(text[i + 1] ?? "");
56 i += 2;
57 continue;
58 }
59 if (text[i] === "\n") break;
60 out += text[i++];
61 }
62 i++;
63 return out;
64 };
65
66 const template = (): string => {
67 let out = "";
68 i++;
69 while (i < n && text[i] !== "`") {
70 if (text[i] === "\\") {
71 out += unescape(text[i + 1] ?? "");
72 i += 2;
73 } else if (text[i] === "$" && text[i + 1] === "{") {
74 i += 2;
75 skipCode("}");
76 i++;
77 out += HOLE;
78 } else out += text[i++];
79 }
80 i++;
81 return out;
82 };
83
84 // Walks code until `close` at depth zero; literals inside a `${…}` are still found.
85 const skipCode = (close: string | null): void => {
86 let depth = 0;
87 while (i < n) {
88 const c = text[i]!;
89 if (close !== null && c === close && depth === 0) return;
90 if (c === "{") depth++;
91 else if (c === "}") depth--;
92 if (c === "/" && text[i + 1] === "/") {
93 while (i < n && text[i] !== "\n") i++;
94 continue;
95 }
96 if (c === "/" && text[i + 1] === "*") {
97 const end = text.indexOf("*/", i + 2);
98 i = end < 0 ? n : end + 2;
99 continue;
100 }
101 if (c === '"' || c === "'" || c === "`") {
102 const start = i;
103 const value = c === "`" ? template() : quoted(c);
104 found.push({ start, end: i, value });
105 last = c;
106 continue;
107 }
108 if (c === "/" && (last === "" || "(,=:[!&|?{};+-*%<>~^".includes(last) || /\b(?:return|typeof|case)$/.test(text.slice(Math.max(0, i - 7), i).trimEnd()))) {
109 i++;
110 let inClass = false;
111 while (i < n && text[i] !== "\n") {
112 if (text[i] === "\\") i++;
113 else if (text[i] === "[") inClass = true;
114 else if (text[i] === "]") inClass = false;
115 else if (text[i] === "/" && !inClass) break;
116 i++;
117 }
118 i++;
119 last = "/";
120 continue;
121 }
122 if (!/\s/.test(c)) last = c;
123 i++;
124 }
125 };
126
127 skipCode(null);
128 found.sort((a, b) => a.start - b.start);
129 const joined: string[] = [];
130 let prev: { end: number } | null = null;
131 for (const lit of found) {
132 if (prev !== null && joined.length > 0 && /^\s*\+\s*$/.test(text.slice(prev.end, lit.start)))
133 joined[joined.length - 1] += lit.value;
134 else joined.push(lit.value);
135 prev = lit;
136 }
137 return joined;
138 }
139
140 function unescape(c: string): string {
141 if (c === "n" || c === "t" || c === "r") return " ";
142 if (c === "\n") return "";
143 return c;
144 }
145
146 /** The literals of `text` that read as one SQL statement, leading comments dropped. */
147 export function sqlLiterals(text: string): string[] {
148 const out: string[] = [];
149 for (const lit of literals(text)) {
150 const sql = lit.replace(/^(?:\s+|--[^\n]*\n?|\/\*[\s\S]*?\*\/)*/, "").trim();
151 if (SQL_START.test(sql) && /\s/.test(sql)) out.push(sql);
152 }
153 return out;
154 }
155
156 const scan = (root: string, glob: string) => [...new Bun.Glob(glob).scanSync({ cwd: root })].sort();
157
158 /** Every SQL literal under `root`/src outside the migrations, distinct. */
159 export function sourceStatements(root: string): string[] {
160 const all = new Set<string>();
161 for (const f of scan(root, "src/**/*.ts")) {
162 if (f.startsWith("src/core/migrations/") || f.endsWith(".test.ts")) continue;
163 for (const sql of sqlLiterals(readFileSync(join(root, f), "utf8"))) all.add(sql);
164 }
165 return [...all];
166 }
167
168 /** The statements inside each trigger, with `NEW.x` and `OLD.x` bound. */
169 export function triggerStatements(db: Database): string[] {
170 const out: string[] = [];
171 const rows = db
172 .query<{ sql: string }, []>("SELECT sql FROM sqlite_master WHERE type = 'trigger' AND sql IS NOT NULL")
173 .all();
174 for (const { sql } of rows) {
175 const body = /\bBEGIN\b([\s\S]*)\bEND\s*;?\s*$/i.exec(sql)?.[1];
176 if (body === undefined) continue;
177 for (const part of body.split(";")) {
178 const s = part.replace(/\b(?:NEW|OLD)\.["`]?\w+["`]?/gi, "?").trim();
179 if (SQL_START.test(s)) out.push(s);
180 }
181 }
182 return out;
183 }
184
185 /** The forms a `${…}` is tried as, in order, until the statement prepares. */
186 const STAND_INS = ["?", "1", "", "id"];
187
188 /** The index names one statement's plan walks, or null when it prepares in no form. */
189 export function plannedIndexes(db: Database, sql: string): string[] | null {
190 for (const stand of STAND_INS) {
191 const text = sql.split(HOLE).join(stand);
192 let plan: { detail: string }[];
193 try {
194 plan = db.query<{ detail: string }, []>(`EXPLAIN QUERY PLAN ${text}`).all();
195 } catch {
196 continue;
197 }
198 const names = new Set<string>();
199 for (const { detail } of plan)
200 for (const m of detail.matchAll(/USING (?:COVERING )?INDEX (\S+)/g)) names.add(m[1]!);
201 return [...names];
202 }
203 return null;
204 }
205
206 export interface IndexRow {
207 name: string;
208 table: string;
209 sql: string;
210 /** Why it may never be dropped; empty when nothing keeps it but use. */
211 kept: string[];
212 /** The statements whose plan walks it. */
213 usedBy: string[];
214 }
215
216 export interface IndexUse {
217 indexes: IndexRow[];
218 /** Statements that prepared in no form (dynamic SQL); counted, not planned. */
219 unplanned: string[];
220 }
221
222 /** Each explicit index of `db`, what uses it and what keeps it. */
223 export function indexUse(db: Database, statements: readonly string[], named: ReadonlySet<string>): IndexUse {
224 const rows = db
225 .query<{ name: string; tbl_name: string; sql: string }, []>(
226 "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL ORDER BY tbl_name, name",
227 )
228 .all();
229 const byName = new Map<string, IndexRow>();
230 for (const r of rows)
231 byName.set(r.name, { name: r.name, table: r.tbl_name, sql: r.sql, kept: keptBecause(db, r, named), usedBy: [] });
232 const unplanned: string[] = [];
233 for (const sql of statements) {
234 const used = plannedIndexes(db, sql);
235 if (used === null) {
236 unplanned.push(sql);
237 continue;
238 }
239 for (const name of used) byName.get(name)?.usedBy.push(sql);
240 }
241 return { indexes: [...byName.values()], unplanned };
242 }
243
244 function keptBecause(db: Database, r: { name: string; tbl_name: string; sql: string }, named: ReadonlySet<string>): string[] {
245 const why: string[] = [];
246 if (/^\s*CREATE\s+UNIQUE\b/i.test(r.sql)) why.push("unique");
247 if (/\)\s*WHERE\b/i.test(r.sql)) why.push("partial");
248 const cols = db
249 .query<{ name: string | null }, [string]>("SELECT name FROM pragma_index_info(?1) ORDER BY seqno")
250 .all(r.name)
251 .map((c) => c.name);
252 const fks = new Map<number, string[]>();
253 for (const fk of db
254 .query<{ id: number; from: string }, [string]>('SELECT id, "from" FROM pragma_foreign_key_list(?1)')
255 .all(r.tbl_name))
256 fks.set(fk.id, [...(fks.get(fk.id) ?? []), fk.from]);
257 for (const from of fks.values()) {
258 const lead = new Set(cols.slice(0, from.length));
259 if (from.every((c) => lead.has(c))) {
260 why.push("foreign key");
261 break;
262 }
263 }
264 if (named.has(r.name)) why.push("named in code");
265 return why;
266 }
267
268 /** The index names `src/` and `test/` mention outside the migrations. */
269 export function namedIndexes(root: string, names: readonly string[]): Set<string> {
270 const files = [...scan(root, "src/**/*.ts"), ...scan(root, "test/**/*.ts")].filter(
271 (f) => !f.startsWith("src/core/migrations/"),
272 );
273 const text = files.map((f) => readFileSync(join(root, f), "utf8")).join("\n");
274 const words = new Set(text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []);
275 return new Set(names.filter((n) => words.has(n)));
276 }
277
278 /** The indexes nothing plans through and nothing keeps. */
279 export function unusedIndexes(use: IndexUse): IndexRow[] {
280 return use.indexes.filter((r) => r.usedBy.length === 0 && r.kept.length === 0);
281 }
282
283 /**
284 * A copy of `db` with `rows` rows in every table, values made from each
285 * column's declared type, so an index holds what it would hold in use.
286 * Checks, foreign keys and triggers are off in the copy: shape, not truth.
287 */
288 export function seeded(db: Database, rows: number): Database {
289 const { Database: Db } = require("bun:sqlite") as typeof import("bun:sqlite");
290 const copy = Db.deserialize(db.serialize());
291 copy.exec("PRAGMA foreign_keys = OFF; PRAGMA ignore_check_constraints = ON; PRAGMA journal_mode = MEMORY;");
292 for (const { name } of copy.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'trigger'").all())
293 copy.exec(`DROP TRIGGER "${name}"`);
294 const tables = copy
295 .query<{ name: string }, []>(
296 "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND sql NOT LIKE 'CREATE VIRTUAL%'",
297 )
298 .all();
299 copy.exec("BEGIN");
300 for (const { name } of tables) {
301 const cols = copy
302 .query<{ name: string; type: string; hidden: number }, [string]>(
303 "SELECT name, type, hidden FROM pragma_table_xinfo(?1) WHERE hidden = 0",
304 )
305 .all(name);
306 // A text is a JSON string, so a generated column that reads JSON reads it.
307 try {
308 const insert = copy.prepare(
309 `INSERT OR IGNORE INTO "${name}" (${cols.map((c) => `"${c.name}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`,
310 );
311 for (let i = 0; i < rows; i++)
312 insert.run(...cols.map((c, k) => (/INT|REAL|NUM/i.test(c.type) ? 1_700_000_000_000 + i * 7919 + k : `"${c.name}-${i}-${(i * 2654435761) % 1_000_003}"`)));
313 } catch {
314 // A table this cannot fill (a full-text shadow, a generated column it cannot satisfy)
315 // stays as full as it got; its indexes count what it holds.
316 }
317 }
318 copy.exec("COMMIT");
319 return copy;
320 }
321
322 /** The bytes the file of `db` loses when `names` are dropped, after a VACUUM. */
323 export function bytesHeld(db: Database, names: readonly string[]): number {
324 const { Database: Db } = require("bun:sqlite") as typeof import("bun:sqlite");
325 const size = (d: Database) => {
326 d.exec("VACUUM");
327 const { n } = d.query<{ n: number }, []>("SELECT page_count * page_size AS n FROM pragma_page_count, pragma_page_size").get()!;
328 return n;
329 };
330 const without = Db.deserialize(db.serialize());
331 for (const name of names) without.exec(`DROP INDEX "${name}"`);
332 const before = size(Db.deserialize(db.serialize()));
333 return before - size(without);
334 }
335
336 /** Appends `sql` to the file `FORGE_SQL_LOG` names, once per process per text (the test run's collector). */
337 export function collectInto(file: string): (sql: string) => void {
338 const seen = new Set<string>();
339 return (sql) => {
340 if (seen.has(sql)) return;
341 seen.add(sql);
342 appendFileSync(file, `${JSON.stringify(sql)}\n`);
343 };
344 }
345
346 /** The statements a collector wrote, one JSON string per line. */
347 export function readLog(file: string): string[] {
348 return readFileSync(file, "utf8")
349 .split("\n")
350 .filter((l) => l.trim() !== "")
351 .map((l) => JSON.parse(l) as string)
352 .filter((s) => SQL_START.test(s.trim()));
353 }
354
355 if (import.meta.main) {
356 const { openDb } = await import("../src/kernel/db.ts");
357 const { migrations } = await import("../src/core/schema.ts");
358 const args = process.argv.slice(2);
359 const root = join(import.meta.dir, "..");
360 const db = openDb(":memory:", migrations);
361 const statements = new Set([...sourceStatements(root), ...triggerStatements(db)]);
362 for (let k = 0; k < args.length; k++) if (args[k] === "--log") for (const s of readLog(args[++k]!)) statements.add(s);
363 const names = db.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL").all().map((r) => r.name);
364 const use = indexUse(db, [...statements], namedIndexes(root, names));
365 const unused = unusedIndexes(use);
366 const one = (s: string) => s.replace(/\s+/g, " ").replaceAll(HOLE, "${…}").slice(0, 160);
367 // CLI answer for the person at the terminal.
368 if (args.includes("--unused") || args.includes("--bytes")) {
369 const seed = args.includes("--bytes") ? seeded(db, 1000) : null;
370 let total = 0;
371 for (const r of unused) {
372 const bytes = seed === null ? null : bytesHeld(seed, [r.name]);
373 total += bytes ?? 0;
374 console.log(bytes === null ? `${r.name}\t${r.table}` : `${r.name}\t${r.table}\t${bytes}`);
375 }
376 if (seed !== null) console.log(`total\t${unused.length} indexes\t${bytesHeld(seed, unused.map((r) => r.name))} bytes (sum of each alone ${total}) at 1000 rows a table`);
377 } else {
378 for (const r of use.indexes) {
379 console.log(`${r.name} on ${r.table}${r.kept.length > 0 ? ` (kept: ${r.kept.join(", ")})` : ""}: ${r.usedBy.length} statements`);
380 for (const s of r.usedBy) console.log(` ${one(s)}`);
381 }
382 }
383 console.error(
384 `index-use: ${statements.size} statements, ${use.unplanned.length} would not prepare, ${use.indexes.length} indexes, ${unused.length} unused`,
385 );
386 }
src/core/migration-pins/20261113115530-drop-unused-indexes.pin added +1
@@ -0,0 +1,1 @@
1 63971e93a95da2f67d77bca205bc754e49ccd8031efc1293403e79c2fcebb1e6
src/core/migrations/20261113115530-drop-unused-indexes.ts added +17
@@ -0,0 +1,17 @@
1 // Seven indexes no statement the forge prepares plans through (#12011, part of #11973:
2 // indexes are most of forge.db). `bun scripts/index-use.ts --unused` finds them: every SQL
3 // text under src/ and in the triggers, EXPLAIN QUERY PLAN on a fresh schema; the forge never
4 // runs ANALYZE, so production plans the same way. None is UNIQUE, partial, a foreign key's
5 // lookup or named by code. Each table's reads already walk another index or the key:
6 // session by session_user, storage by storage_at, webhook_delivery by delivery_state_id,
7 // note_shipped by its key, the rest read whole or by id. Writes stop paying for them.
8 // Additive: an old process plans the same statements without them.
9 export default `
10 DROP INDEX IF EXISTS flow_valve_turn_at;
11 DROP INDEX IF EXISTS gift_redeem_code;
12 DROP INDEX IF EXISTS note_shipped_pull;
13 DROP INDEX IF EXISTS session_seen;
14 DROP INDEX IF EXISTS storage_repo;
15 DROP INDEX IF EXISTS telemetry_read_at;
16 DROP INDEX IF EXISTS delivery_due;
17 `;
test/helpers/preload.ts +16
@@ -23,3 +23,19 @@ process.env.FORGE_DEV = "1";
2323
2424 // A suite has no public network to prove a child through (`spawn/reach.ts`); a test of reach injects its own fetch.
2525 useReachByDefault({ off: true });
26
27 // `FORGE_SQL_LOG=<file>`: each distinct statement the run prepares, for
28 // `scripts/index-use.ts --log` (#12011). Off unless asked for.
29 if (process.env.FORGE_SQL_LOG) {
30 const { Database } = await import("bun:sqlite");
31 const { collectInto } = await import("../../scripts/index-use.ts");
32 const note = collectInto(process.env.FORGE_SQL_LOG);
33 const proto = Database.prototype as unknown as Record<"query" | "prepare", (sql: string, ...rest: unknown[]) => unknown>;
34 for (const method of ["query", "prepare"] as const) {
35 const original = proto[method];
36 proto[method] = function (this: unknown, sql: string, ...rest: unknown[]) {
37 if (typeof sql === "string") note(sql);
38 return original.call(this, sql, ...rest);
39 };
40 }
41 }
test/index-use.test.ts added +130
@@ -0,0 +1,130 @@
1 /**
2 * `scripts/index-use.ts` (#12011): which index each prepared statement walks,
3 * and which indexes nothing uses and nothing keeps. The migration that drops
4 * the seven it found leaves a fresh database without them.
5 */
6
7 import { Database } from "bun:sqlite";
8 import { describe, expect, test } from "bun:test";
9 import { mkdtempSync, rmSync } from "node:fs";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { migrations } from "../src/core/schema.ts";
13 import { openDb } from "../src/kernel/db.ts";
14 import {
15 bytesHeld,
16 collectInto,
17 indexUse,
18 plannedIndexes,
19 readLog,
20 seeded,
21 sqlLiterals,
22 triggerStatements,
23 unusedIndexes,
24 } from "../scripts/index-use.ts";
25
26 describe("SQL in source", () => {
27 test("string, template and joined literals that read as SQL, with holes for `${…}`", () => {
28 const text = [
29 'const a = db.query("SELECT id FROM run WHERE state = ?1");',
30 "const b = db.query(`",
31 " -- the queued ones",
32 " SELECT id FROM run WHERE id IN (${marks.map(() => `?`).join(',')})`);",
33 'const c = "SELECT x " +',
34 ' "FROM t";',
35 "const re = /it's \"not\" a string/;",
36 'const d = "not sql at all";',
37 'const e = "SELECTED";',
38 ].join("\n");
39 expect(sqlLiterals(text)).toEqual([
40 "SELECT id FROM run WHERE state = ?1",
41 "SELECT id FROM run WHERE id IN (\u0000)",
42 "SELECT x FROM t",
43 ]);
44 });
45 });
46
47 function small(): Database {
48 const db = new Database(":memory:");
49 db.exec(`
50 CREATE TABLE parent (id INTEGER PRIMARY KEY);
51 CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT, state TEXT, parent_id INTEGER REFERENCES parent(id));
52 CREATE INDEX t_a ON t(a);
53 CREATE INDEX t_b ON t(b);
54 CREATE UNIQUE INDEX t_ab ON t(a, b);
55 CREATE INDEX t_live ON t(a) WHERE state = 'live';
56 CREATE INDEX t_parent ON t(parent_id, a);
57 CREATE INDEX t_named ON t(state, b);
58 CREATE TABLE log (id INTEGER PRIMARY KEY, b TEXT);
59 CREATE TRIGGER t_log AFTER INSERT ON t BEGIN INSERT INTO log (b) SELECT b FROM t WHERE b = NEW.b; END;
60 `);
61 return db;
62 }
63
64 describe("index use", () => {
65 test("each index lists the statements whose plan walks it", () => {
66 const db = small();
67 expect(plannedIndexes(db, "SELECT id FROM t WHERE b = ?1")).toEqual(["t_b"]);
68 expect(plannedIndexes(db, "SELECT id FROM t WHERE id = ?1")).toEqual([]);
69 expect(plannedIndexes(db, "SELECT id FROM \u0000 WHERE b = ?1")).toBeNull();
70 const use = indexUse(db, ["SELECT id FROM t WHERE b = ?1", "SELECT nonsense FROM nowhere"], new Set());
71 expect(use.indexes.find((r) => r.name === "t_b")!.usedBy).toEqual(["SELECT id FROM t WHERE b = ?1"]);
72 expect(use.unplanned).toEqual(["SELECT nonsense FROM nowhere"]);
73 });
74
75 test("unique, partial, a foreign key's lookup and a name code says are kept; the rest unused", () => {
76 const db = small();
77 const use = indexUse(db, [], new Set(["t_named"]));
78 const kept = Object.fromEntries(use.indexes.map((r) => [r.name, r.kept]));
79 expect(kept).toEqual({
80 t_a: [],
81 t_ab: ["unique"],
82 t_b: [],
83 t_live: ["partial"],
84 t_named: ["named in code"],
85 t_parent: ["foreign key"],
86 });
87 expect(unusedIndexes(use).map((r) => r.name)).toEqual(["t_a", "t_b"]);
88 });
89
90 test("a trigger's statements are planned, NEW and OLD bound", () => {
91 const db = small();
92 const inside = triggerStatements(db);
93 expect(inside).toEqual(["INSERT INTO log (b) SELECT b FROM t WHERE b = ?"]);
94 const use = indexUse(db, inside, new Set());
95 expect(unusedIndexes(use).map((r) => r.name)).toEqual(["t_a", "t_named"]);
96 });
97
98 test("a seeded copy counts the bytes an index holds", () => {
99 const db = small();
100 const copy = seeded(db, 2000);
101 expect(copy.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM t").get()!.n).toBe(2000);
102 expect(bytesHeld(copy, ["t_b"])).toBeGreaterThan(4096);
103 expect(db.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM t").get()!.n).toBe(0);
104 });
105
106 test("a test run's collector writes each statement once and the script reads them back", () => {
107 const dir = mkdtempSync(join(tmpdir(), "index-use-"));
108 try {
109 const file = join(dir, "sql.log");
110 const note = collectInto(file);
111 note("SELECT 1 FROM t");
112 note("SELECT 1 FROM t");
113 note("PRAGMA foreign_keys = ON");
114 note("UPDATE t SET a = 'x\ny'");
115 expect(readLog(file)).toEqual(["SELECT 1 FROM t", "UPDATE t SET a = 'x\ny'"]);
116 } finally {
117 rmSync(dir, { recursive: true, force: true });
118 }
119 });
120 });
121
122 test("a fresh forge database has none of the seven indexes nothing used", () => {
123 const db = openDb(":memory:", migrations);
124 const gone = ["flow_valve_turn_at", "gift_redeem_code", "note_shipped_pull", "session_seen", "storage_repo", "telemetry_read_at", "delivery_due"];
125 const left = db
126 .query<{ name: string }, []>(`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN (${gone.map((n) => `'${n}'`).join(", ")})`)
127 .all();
128 expect(left).toEqual([]);
129 db.close();
130 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs