diff --git a/__tests__/schemaless-db-not-initialized.test.ts b/__tests__/schemaless-db-not-initialized.test.ts index dc960934b..484f49616 100644 --- a/__tests__/schemaless-db-not-initialized.test.ts +++ b/__tests__/schemaless-db-not-initialized.test.ts @@ -178,4 +178,33 @@ describe('a schema-less codegraph.db is not an initialized project (#1895)', () db.close(); expect(row.c).toBe(1); }); + + it('(d) an empty file is repaired too; a file that is not SQLite is refused with guidance and left alone', () => { + plantBrokenDb(parent, 'empty'); + fs.writeFileSync(path.join(parent, 'a.py'), 'def alpha():\n return 1\n'); + const empty = run(parent, ['init', '--yes']); + expect(empty.status).toBe(0); + expect(empty.out).toContain('without the codegraph schema'); + expect(isInitialized(parent)).toBe(true); + + for (const bytes of ['x'.repeat(4096), 'hello']) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1895-foreign-')); + try { + const cgDir = path.join(dir, '.codegraph'); + fs.mkdirSync(cgDir); + const dbPath = path.join(cgDir, 'codegraph.db'); + fs.writeFileSync(dbPath, bytes); + fs.writeFileSync(path.join(dir, 'a.py'), 'def alpha():\n return 1\n'); + const r = run(dir, ['init', '--yes']); + expect(r.status).toBe(1); + expect(r.out).toContain('is not a SQLite database'); + expect(r.out).not.toContain('rebuilding'); + expect(r.out).not.toContain('file is not a database'); + // Nothing is deleted or rewritten behind the user's back. + expect(fs.readFileSync(dbPath, 'utf-8')).toBe(bytes); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + } + }); }); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 4729cb60d..7ecc3503a 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -41,7 +41,7 @@ try { import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; -import { getCodeGraphDir, isInitialized, hasSchemalessDb, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; +import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; import { extractProseCandidates } from '../search/identifier-segments'; import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree'; import { createShimmerProgress } from '../ui/shimmer-progress'; @@ -697,6 +697,14 @@ async function runInit( return; } + if (hasForeignDbFile(projectPath)) { + const dbFile = path.join(getCodeGraphDir(projectPath), 'codegraph.db'); + clack.log.error(`${dbFile} is not a SQLite database, so it cannot be rebuilt in place.`); + clack.log.info('Move or delete that file, then run "codegraph init" again.'); + clack.outro(''); + process.exitCode = 1; + return; + } if (hasSchemalessDb(projectPath)) { clack.log.warn(`Found a codegraph.db without the codegraph schema in ${getCodeGraphDir(projectPath)} (left by an interrupted init?) — rebuilding it.`); } diff --git a/src/directory.ts b/src/directory.ts index f065821a0..e1a7cf7f6 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -119,10 +119,31 @@ export function isInitialized(projectRoot: string): boolean { return hasCodeGraphSchema(dbPath, st); } -/** `codegraph.db` exists at `projectRoot` but does not carry the schema (#1895). */ +/** + * `codegraph.db` exists at `projectRoot` but does not carry the schema, and + * `init` can add it in place: an empty file, or a SQLite database without the + * codegraph tables (#1895). A file that is not SQLite at all is NOT this case — + * see {@link hasForeignDbFile}. + */ export function hasSchemalessDb(projectRoot: string): boolean { const dbPath = path.join(getCodeGraphDir(projectRoot), 'codegraph.db'); - return fs.existsSync(dbPath) && !isInitialized(projectRoot); + let st: fs.Stats; + try { st = fs.statSync(dbPath); } catch { return false; } + if (!st.isFile() || isInitialized(projectRoot)) return false; + return st.size === 0 || readsAsSqlite(dbPath); +} + +/** + * `codegraph.db` exists at `projectRoot` and is not a SQLite database (no + * header magic): SQLite refuses to open it, so `init` cannot rebuild it in + * place. The caller must say so rather than promise a repair; nothing here + * deletes the file. + */ +export function hasForeignDbFile(projectRoot: string): boolean { + const dbPath = path.join(getCodeGraphDir(projectRoot), 'codegraph.db'); + let st: fs.Stats; + try { st = fs.statSync(dbPath); } catch { return false; } + return st.isFile() && st.size > 0 && !readsAsSqlite(dbPath); } const SQLITE_MAGIC = Buffer.from('SQLite format 3\0', 'latin1');