Skip to content

Commit c71da13

Browse files
Brooooooklynclaude
andcommitted
fix(db): export every MySQL table and restore with foreign keys off
Bug: `void db export` in an app with "database": "mysql" fails on MySQL 8 with "Table 'app.undefined' doesn't exist" (with or without --output, --no-schema, --no-data). Once the table names are read correctly, the dump still cannot be restored when a child table's name sorts before its parent's: a full dump fails with "Failed to open the referenced table", and a --no-schema dump fails with "Cannot add or update a child row: a foreign key constraint fails". Repro: on MySQL 8, create posts(id PK) and comments(post_id FK -> posts.id) with one row each, then run `void db export --output dump.sql`. It throws. With the name read fixed, load the dump into an empty database: comments is created (or filled) before posts and the foreign key check rejects it. Cause: runExport in packages/void/src/cli/db.ts lists tables with an unaliased `SELECT table_name FROM information_schema.tables`. MySQL 8 labels that column `TABLE_NAME`, so `row.table_name` is undefined and every table becomes "undefined". The loop then writes tables in name order, SHOW CREATE TABLE keeps foreign keys inline, and the dump never turns FOREIGN_KEY_CHECKS off, so restore order breaks on parent/child pairs. Fix: alias the column (`table_name AS table_name`), the same fix `void db reset` already uses, and wrap the MySQL dump in `SET FOREIGN_KEY_CHECKS = 0;` ... `SET FOREIGN_KEY_CHECKS = 1;`. The wrapper is smaller than sorting tables by dependency and also covers foreign key cycles. Only the MySQL branch of runExport changes. Test: packages/void/test/unit/db-commands.test.ts, "void db export > MySQL: exports every table with foreign key checks off". Without the fix the export queries `undefined` and has no SET lines. Note: every MySQL export (full, --table, --no-schema, --no-data) now starts with FOREIGN_KEY_CHECKS = 0 and ends with = 1, the MySQL default. A restore into a session that already had the checks off ends with them on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
1 parent e17f70f commit c71da13

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

‎packages/void/src/cli/db.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1832,12 +1832,16 @@ async function runExport(
18321832
}
18331833
tables = [args.table];
18341834
} else {
1835+
// MySQL 8 returns an unaliased information_schema column as `TABLE_NAME`.
18351836
const rows = await client.query<Array<{ table_name: string }>>(
1836-
"SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' AND table_name NOT LIKE '\\_%' ORDER BY table_name",
1837+
"SELECT table_name AS table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' AND table_name NOT LIKE '\\_%' ORDER BY table_name",
18371838
);
18381839
tables = rows.map((row) => String(row.table_name));
18391840
}
18401841

1842+
// Tables come out in name order with inline foreign keys, so a child
1843+
// table can precede its parent; restore with the checks off.
1844+
parts.push('SET FOREIGN_KEY_CHECKS = 0;');
18411845
for (const table of tables) {
18421846
const escapedTable = table.replaceAll('`', '``');
18431847
if (!args.noSchema) {
@@ -1863,6 +1867,7 @@ async function runExport(
18631867
}
18641868
parts.push('');
18651869
}
1870+
parts.push('SET FOREIGN_KEY_CHECKS = 1;', '');
18661871
});
18671872

18681873
const output = parts.join('\n');

‎packages/void/test/unit/db-commands.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,47 @@ describe('void db reset', () => {
451451
});
452452
});
453453

454+
describe('void db export', () => {
455+
let runDbCommand: typeof import('../../src/cli/db.ts').runDbCommand;
456+
457+
beforeEach(async () => {
458+
vi.clearAllMocks();
459+
vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
460+
throw new Error(`process.exit:${code ?? 0}`);
461+
}) as never);
462+
runDbCommand = (await import('../../src/cli/db.ts')).runDbCommand;
463+
});
464+
465+
afterEach(() => {
466+
vi.restoreAllMocks();
467+
for (const root of rootsToClean.splice(0, rootsToClean.length)) {
468+
rmSync(root, { recursive: true, force: true });
469+
}
470+
});
471+
472+
it('MySQL: exports every table with foreign key checks off', async () => {
473+
const root = mkdtempSync(join(tmpdir(), 'void-export-mysql-'));
474+
rootsToClean.push(root);
475+
writeFileSync(join(root, 'void.json'), JSON.stringify({ database: 'mysql' }));
476+
writeFileSync(join(root, '.env'), 'DATABASE_URL="mysql://root@127.0.0.1:3306/app"\n');
477+
mysqlQueries.length = 0;
478+
let stdout = '';
479+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
480+
stdout += chunk.toString();
481+
return true;
482+
});
483+
484+
await runDbCommand(root, { action: 'export' });
485+
486+
expect(mysqlQueries).toContain('SHOW CREATE TABLE `users`');
487+
expect(mysqlQueries).toContain('SELECT * FROM `users`');
488+
expect(mysqlQueries.join('\n')).not.toContain('`undefined`');
489+
const lines = stdout.trim().split('\n');
490+
expect(lines[0]).toBe('SET FOREIGN_KEY_CHECKS = 0;');
491+
expect(lines.at(-1)).toBe('SET FOREIGN_KEY_CHECKS = 1;');
492+
});
493+
});
494+
454495
describe('void db status', () => {
455496
let runDbCommand: typeof import('../../src/cli/db.ts').runDbCommand;
456497

0 commit comments

Comments
 (0)