| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- #!/usr/bin/env bun
- /**
- * Migrate documents table from collection_id to collection name
- *
- * This script updates the database schema to use collection names
- * instead of collection_id foreign keys, preparing for YAML-based
- * collection management.
- */
- import { Database } from "bun:sqlite";
- import { join } from "path";
- import { homedir } from "os";
- const c = {
- reset: "\x1b[0m",
- cyan: "\x1b[36m",
- green: "\x1b[32m",
- yellow: "\x1b[33m",
- dim: "\x1b[2m",
- };
- const dbPath = join(homedir(), ".cache", "qmd", "index.sqlite");
- console.log(`${c.cyan}Migrating database schema...${c.reset}\n`);
- console.log(`Database: ${dbPath}\n`);
- const db = new Database(dbPath);
- try {
- db.exec("BEGIN TRANSACTION");
- // Step 1: Add collection column to documents
- console.log(`${c.yellow}1. Adding 'collection' column to documents table...${c.reset}`);
- db.exec(`ALTER TABLE documents ADD COLUMN collection TEXT`);
- console.log(` ${c.green}✓${c.reset} Column added`);
- // Step 2: Populate collection names from collections table
- console.log(`\n${c.yellow}2. Populating collection names...${c.reset}`);
- const result = db.exec(`
- UPDATE documents
- SET collection = (
- SELECT name FROM collections WHERE collections.id = documents.collection_id
- )
- WHERE collection IS NULL
- `);
- console.log(` ${c.green}✓${c.reset} Updated ${result} rows`);
- // Step 3: Verify no NULL values
- const nullCount = db.query<{ count: number }, []>(
- `SELECT COUNT(*) as count FROM documents WHERE collection IS NULL`
- ).get();
- if (nullCount && nullCount.count > 0) {
- throw new Error(`Found ${nullCount.count} documents with NULL collection names`);
- }
- console.log(` ${c.green}✓${c.reset} All documents have collection names`);
- // Step 4: Create new documents table without collection_id
- console.log(`\n${c.yellow}3. Creating new documents table...${c.reset}`);
- db.exec(`
- CREATE TABLE documents_new (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- collection TEXT NOT NULL,
- path TEXT NOT NULL,
- title TEXT NOT NULL,
- hash TEXT NOT NULL,
- created_at TEXT NOT NULL,
- modified_at TEXT NOT NULL,
- active INTEGER DEFAULT 1,
- FOREIGN KEY (hash) REFERENCES content(hash) ON DELETE CASCADE,
- UNIQUE(collection, path)
- )
- `);
- console.log(` ${c.green}✓${c.reset} New table created`);
- // Step 5: Copy data
- console.log(`\n${c.yellow}4. Copying data to new table...${c.reset}`);
- db.exec(`
- INSERT INTO documents_new (id, collection, path, title, hash, created_at, modified_at, active)
- SELECT id, collection, path, title, hash, created_at, modified_at, active
- FROM documents
- `);
- const rowCount = db.query<{ count: number }, []>(
- `SELECT COUNT(*) as count FROM documents_new`
- ).get();
- console.log(` ${c.green}✓${c.reset} Copied ${rowCount?.count} documents`);
- // Step 6: Drop old table and rename new one
- console.log(`\n${c.yellow}5. Replacing old table...${c.reset}`);
- db.exec(`DROP TABLE documents`);
- db.exec(`ALTER TABLE documents_new RENAME TO documents`);
- console.log(` ${c.green}✓${c.reset} Table replaced`);
- // Step 7: Recreate indices
- console.log(`\n${c.yellow}6. Recreating indices...${c.reset}`);
- db.exec(`CREATE INDEX idx_documents_collection ON documents(collection, active)`);
- db.exec(`CREATE INDEX idx_documents_hash ON documents(hash)`);
- console.log(` ${c.green}✓${c.reset} Indices created`);
- // Step 8: Update FTS trigger to use collection name
- console.log(`\n${c.yellow}7. Updating FTS trigger...${c.reset}`);
- db.exec(`DROP TRIGGER IF EXISTS documents_ai`);
- db.exec(`
- CREATE TRIGGER documents_ai AFTER INSERT ON documents
- WHEN new.active = 1
- BEGIN
- INSERT INTO documents_fts(rowid, filepath, title, body)
- SELECT
- new.id,
- new.collection || '/' || new.path,
- new.title,
- (SELECT doc FROM content WHERE hash = new.hash)
- WHERE new.active = 1;
- END
- `);
- db.exec(`DROP TRIGGER IF EXISTS documents_au`);
- db.exec(`
- CREATE TRIGGER documents_au AFTER UPDATE ON documents
- BEGIN
- -- Delete from FTS if no longer active
- DELETE FROM documents_fts WHERE rowid = old.id AND new.active = 0;
- -- Update FTS if still/newly active
- INSERT OR REPLACE INTO documents_fts(rowid, filepath, title, body)
- SELECT
- new.id,
- new.collection || '/' || new.path,
- new.title,
- (SELECT doc FROM content WHERE hash = new.hash)
- WHERE new.active = 1;
- END
- `);
- console.log(` ${c.green}✓${c.reset} Triggers updated`);
- // Commit transaction
- db.exec("COMMIT");
- console.log(`\n${c.green}✓ Migration completed successfully!${c.reset}`);
- // Show summary
- const collections = db.query<{ collection: string; count: number }, []>(`
- SELECT collection, COUNT(*) as count
- FROM documents
- WHERE active = 1
- GROUP BY collection
- ORDER BY collection
- `).all();
- console.log(`\n${c.dim}Documents by collection:${c.reset}`);
- for (const coll of collections) {
- console.log(` ${coll.collection}: ${coll.count} files`);
- }
- } catch (error) {
- db.exec("ROLLBACK");
- console.error(`\n${c.yellow}✗ Migration failed:${c.reset} ${error}`);
- console.error(`${c.dim}Database rolled back to previous state${c.reset}`);
- process.exit(1);
- } finally {
- db.close();
- }
|