Skip to content

Commit e08716e

Browse files
committed
test: pick the tags leaf page by walking the SQLite b-tree, not by scanning content
1 parent ee8b7b3 commit e08716e

1 file changed

Lines changed: 21 additions & 27 deletions

File tree

packages/cli/src/commands/doctor-repair-db.test.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -166,42 +166,36 @@ function seedCurrentDatabase(dbPath: string): void {
166166
}
167167

168168
function corruptLastTagLeaf(dbPath: string): void {
169+
// Zero exactly one LEAF page of the `tags` b-tree, so most rows survive and `.recover`
170+
// has something to salvage. Two approaches are unavailable here: `dbstat` is a
171+
// compile-time SQLite option (SQLITE_ENABLE_DBSTAT_VTAB) that some bun builds omit, and
172+
// scanning for row text picks a different page depending on how the build packs cells —
173+
// both make the fixture depend on the toolchain rather than on the code under test.
174+
// Instead walk the documented on-disk format: sqlite_master gives the table's root page,
175+
// and the page header's rightmost-child pointer descends to the last leaf.
169176
const db = new Database(dbPath, { readonly: true });
170177
const { page_size: pageSize } = db.prepare("PRAGMA page_size").get() as { page_size: number };
171-
// `dbstat` is a compile-time SQLite option (SQLITE_ENABLE_DBSTAT_VTAB), present in some
172-
// bun builds and absent in others, so picking a page through it makes this fixture pass
173-
// or fail on the toolchain rather than on the code under test. Scan the file instead.
174-
// Every `tags` row stores the literal 'message' in its type column and nothing bulky,
175-
// so a tags leaf packs dozens of them into one page while other pages that mention the
176-
// word (the schema page's start_message/end_message columns) hold only a few. Taking
177-
// the page with the MOST occurrences therefore lands on a real tags leaf without a
178-
// magic threshold; skip page 1, which is the schema header.
179-
const { page_count: pageCount } = db.prepare("PRAGMA page_count").get() as {
180-
page_count: number;
181-
};
178+
const { rootpage } = db
179+
.prepare("SELECT rootpage FROM sqlite_master WHERE type = 'table' AND name = 'tags'")
180+
.get() as { rootpage: number };
182181
db.close();
183182

184-
const rowMarker = Buffer.from("message");
185183
const fd = openSync(dbPath, "r+");
186184
try {
187185
const buffer = Buffer.alloc(pageSize);
188-
let target: number | undefined;
189-
let best = 0;
190-
for (let pageno = 2; pageno <= pageCount; pageno++) {
186+
let pageno = rootpage;
187+
// Page 1 carries the 100-byte database header before its b-tree header.
188+
for (let depth = 0; depth < 32; depth++) {
191189
readSync(fd, buffer, 0, pageSize, (pageno - 1) * pageSize);
192-
let count = 0;
193-
let at = buffer.indexOf(rowMarker);
194-
while (at !== -1) {
195-
count += 1;
196-
at = buffer.indexOf(rowMarker, at + rowMarker.length);
197-
}
198-
if (count > best) {
199-
best = count;
200-
target = pageno;
201-
}
190+
const headerAt = pageno === 1 ? 100 : 0;
191+
const pageType = buffer[headerAt];
192+
if (pageType === 0x0d) break; // leaf table page
193+
if (pageType !== 0x05) throw new Error(`unexpected page type 0x${pageType.toString(16)}`);
194+
// Interior table page: the rightmost child pointer lives at header offset 8.
195+
pageno = buffer.readUInt32BE(headerAt + 8);
202196
}
203-
if (!target) throw new Error("no tags leaf page found to corrupt");
204-
writeSync(fd, Buffer.alloc(pageSize), 0, pageSize, (target - 1) * pageSize);
197+
if (buffer[pageno === 1 ? 100 : 0] !== 0x0d) throw new Error("no tags leaf page found");
198+
writeSync(fd, Buffer.alloc(pageSize), 0, pageSize, (pageno - 1) * pageSize);
205199
} finally {
206200
closeSync(fd);
207201
}

0 commit comments

Comments
 (0)