Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions packages/void/src/cli/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,14 @@ function validatePendingMigrations(
}

export async function runDbCommand(root: string, args: DbArgs): Promise<void> {
console.log();
intro(cliTitle('db'));
// `db export` streams raw SQL to stdout for piping — keep the banner off it.
const exportToStdout = args.action === 'export' && !args.output;
if (exportToStdout) {
process.stderr.write('\n');
} else {
console.log();
}
intro(cliTitle('db'), exportToStdout ? { output: process.stderr } : undefined);

const dialect = getDatabaseDialect(readConfig(root));

Expand Down Expand Up @@ -1374,6 +1380,7 @@ async function runExport(
args: { output?: string; noData?: boolean; noSchema?: boolean; table?: string },
dialect: DatabaseDialect,
): Promise<void> {
const errOut = { output: process.stderr };
if (dialect === 'postgresql') {
const parts: Array<string> = [];
await withPgClient(root, async (client) => {
Expand All @@ -1384,7 +1391,7 @@ async function runExport(
[args.table],
);
if (check.rows.length === 0) {
log.error(`db: Table '${args.table}' not found.`);
log.error(`db: Table '${args.table}' not found.`, errOut);
process.exit(1);
}
tables = [args.table];
Expand Down Expand Up @@ -1452,7 +1459,7 @@ async function runExport(
const dbPath = getMiniflareDatabasePath(persistRoot, 'local');

if (!existsSync(dbPath)) {
log.error('No local database found. Run `void dev` first.');
log.error('No local database found. Run `void dev` first.', errOut);
process.exit(1);
}

Expand All @@ -1464,7 +1471,7 @@ async function runExport(
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
.get(args.table);
if (!exists) {
log.error(`db: Table '${args.table}' not found.`);
log.error(`db: Table '${args.table}' not found.`, errOut);
process.exit(1);
}
tables = [args.table];
Expand Down
83 changes: 83 additions & 0 deletions packages/void/test/unit/db-export-stdout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'pathe';
import { afterEach, describe, expect, it, vi } from 'vite-plus/test';

const shared = vi.hoisted(() => ({ dbPath: '' }));

// Mock the SQLite layer directly (no `importActual`) so the test never loads
// better-sqlite3's native binding — it feeds the export path a canned schema.
vi.mock('../../src/migrations/runner', () => {
const fakeDb = {
prepare(sql: string) {
return {
get: () =>
sql.includes('SELECT sql FROM sqlite_master')
? { sql: 'CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL)' }
: undefined,
all: () => {
if (sql.includes('NOT LIKE')) {
return [{ name: 'notes' }];
}
if (sql.startsWith('SELECT * FROM')) {
return [{ id: 1, title: 'hi' }];
}
return [];
},
};
},
close() {},
};
return {
getMiniflareDatabasePath: () => shared.dbPath,
openSqliteDatabase: () => fakeDb,
};
});

const EXPECTED_SQL =
'CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL);\n' +
'INSERT INTO "notes" (id, title) VALUES (1, \'hi\');\n';

describe('void db export → stdout', () => {
const roots: Array<string> = [];

afterEach(() => {
vi.restoreAllMocks();
for (const root of roots.splice(0, roots.length)) {
rmSync(root, { recursive: true, force: true });
}
});

it('streams pure SQL to stdout and routes the banner to stderr', async () => {
const root = mkdtempSync(join(tmpdir(), 'void-db-export-'));
roots.push(root);
writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies: { void: '*' } }));
writeFileSync(join(root, 'void.json'), JSON.stringify({}));
const dbFile = join(root, 'local.sqlite');
writeFileSync(dbFile, '');
shared.dbPath = dbFile;

const { runDbCommand } = await import('../../src/cli/db.ts');

let stdout = '';
let stderr = '';
vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
stdout += chunk.toString();
return true;
});
vi.spyOn(process.stderr, 'write').mockImplementation((chunk: string | Uint8Array) => {
stderr += chunk.toString();
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`process.exit:${code ?? 0}`);
}) as never);

await runDbCommand(root, { action: 'export' });

expect(stdout).toBe(EXPECTED_SQL);
expect(stdout.startsWith('CREATE TABLE')).toBe(true);
expect(stdout).not.toContain('void db');
expect(stderr).toContain('void db');
});
});