Schema changes: - documents.collection_id (INTEGER FK) → documents.collection (TEXT) - Update UNIQUE constraint to (collection, path) - Update indices to use collection name - Update FTS triggers to compute filepath from collection || '/' || path Code changes in store.ts: - Change all function parameters from collectionId: number to collectionName: string - Update all SQL queries to use d.collection instead of d.collection_id - Remove unnecessary JOINs where collection name is already available - Update DocumentResult type: collectionId → collectionName - Update renameCollection() to also update documents.collection Successfully migrated 2309 documents across 6 collections. This prepares for YAML-based collection configuration where collections table will be removed and collection names will be the primary identifier. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
#!/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();
|
|
}
|