migrate-schema.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. #!/usr/bin/env bun
  2. /**
  3. * Migrate documents table from collection_id to collection name
  4. *
  5. * This script updates the database schema to use collection names
  6. * instead of collection_id foreign keys, preparing for YAML-based
  7. * collection management.
  8. */
  9. import { Database } from "bun:sqlite";
  10. import { join } from "path";
  11. import { homedir } from "os";
  12. const c = {
  13. reset: "\x1b[0m",
  14. cyan: "\x1b[36m",
  15. green: "\x1b[32m",
  16. yellow: "\x1b[33m",
  17. dim: "\x1b[2m",
  18. };
  19. const dbPath = join(homedir(), ".cache", "qmd", "index.sqlite");
  20. console.log(`${c.cyan}Migrating database schema...${c.reset}\n`);
  21. console.log(`Database: ${dbPath}\n`);
  22. const db = new Database(dbPath);
  23. try {
  24. db.exec("BEGIN TRANSACTION");
  25. // Step 1: Add collection column to documents
  26. console.log(`${c.yellow}1. Adding 'collection' column to documents table...${c.reset}`);
  27. db.exec(`ALTER TABLE documents ADD COLUMN collection TEXT`);
  28. console.log(` ${c.green}✓${c.reset} Column added`);
  29. // Step 2: Populate collection names from collections table
  30. console.log(`\n${c.yellow}2. Populating collection names...${c.reset}`);
  31. const result = db.exec(`
  32. UPDATE documents
  33. SET collection = (
  34. SELECT name FROM collections WHERE collections.id = documents.collection_id
  35. )
  36. WHERE collection IS NULL
  37. `);
  38. console.log(` ${c.green}✓${c.reset} Updated ${result} rows`);
  39. // Step 3: Verify no NULL values
  40. const nullCount = db.query<{ count: number }, []>(
  41. `SELECT COUNT(*) as count FROM documents WHERE collection IS NULL`
  42. ).get();
  43. if (nullCount && nullCount.count > 0) {
  44. throw new Error(`Found ${nullCount.count} documents with NULL collection names`);
  45. }
  46. console.log(` ${c.green}✓${c.reset} All documents have collection names`);
  47. // Step 4: Create new documents table without collection_id
  48. console.log(`\n${c.yellow}3. Creating new documents table...${c.reset}`);
  49. db.exec(`
  50. CREATE TABLE documents_new (
  51. id INTEGER PRIMARY KEY AUTOINCREMENT,
  52. collection TEXT NOT NULL,
  53. path TEXT NOT NULL,
  54. title TEXT NOT NULL,
  55. hash TEXT NOT NULL,
  56. created_at TEXT NOT NULL,
  57. modified_at TEXT NOT NULL,
  58. active INTEGER DEFAULT 1,
  59. FOREIGN KEY (hash) REFERENCES content(hash) ON DELETE CASCADE,
  60. UNIQUE(collection, path)
  61. )
  62. `);
  63. console.log(` ${c.green}✓${c.reset} New table created`);
  64. // Step 5: Copy data
  65. console.log(`\n${c.yellow}4. Copying data to new table...${c.reset}`);
  66. db.exec(`
  67. INSERT INTO documents_new (id, collection, path, title, hash, created_at, modified_at, active)
  68. SELECT id, collection, path, title, hash, created_at, modified_at, active
  69. FROM documents
  70. `);
  71. const rowCount = db.query<{ count: number }, []>(
  72. `SELECT COUNT(*) as count FROM documents_new`
  73. ).get();
  74. console.log(` ${c.green}✓${c.reset} Copied ${rowCount?.count} documents`);
  75. // Step 6: Drop old table and rename new one
  76. console.log(`\n${c.yellow}5. Replacing old table...${c.reset}`);
  77. db.exec(`DROP TABLE documents`);
  78. db.exec(`ALTER TABLE documents_new RENAME TO documents`);
  79. console.log(` ${c.green}✓${c.reset} Table replaced`);
  80. // Step 7: Recreate indices
  81. console.log(`\n${c.yellow}6. Recreating indices...${c.reset}`);
  82. db.exec(`CREATE INDEX idx_documents_collection ON documents(collection, active)`);
  83. db.exec(`CREATE INDEX idx_documents_hash ON documents(hash)`);
  84. console.log(` ${c.green}✓${c.reset} Indices created`);
  85. // Step 8: Update FTS trigger to use collection name
  86. console.log(`\n${c.yellow}7. Updating FTS trigger...${c.reset}`);
  87. db.exec(`DROP TRIGGER IF EXISTS documents_ai`);
  88. db.exec(`
  89. CREATE TRIGGER documents_ai AFTER INSERT ON documents
  90. WHEN new.active = 1
  91. BEGIN
  92. INSERT INTO documents_fts(rowid, filepath, title, body)
  93. SELECT
  94. new.id,
  95. new.collection || '/' || new.path,
  96. new.title,
  97. (SELECT doc FROM content WHERE hash = new.hash)
  98. WHERE new.active = 1;
  99. END
  100. `);
  101. db.exec(`DROP TRIGGER IF EXISTS documents_au`);
  102. db.exec(`
  103. CREATE TRIGGER documents_au AFTER UPDATE ON documents
  104. BEGIN
  105. -- Delete from FTS if no longer active
  106. DELETE FROM documents_fts WHERE rowid = old.id AND new.active = 0;
  107. -- Update FTS if still/newly active
  108. INSERT OR REPLACE INTO documents_fts(rowid, filepath, title, body)
  109. SELECT
  110. new.id,
  111. new.collection || '/' || new.path,
  112. new.title,
  113. (SELECT doc FROM content WHERE hash = new.hash)
  114. WHERE new.active = 1;
  115. END
  116. `);
  117. console.log(` ${c.green}✓${c.reset} Triggers updated`);
  118. // Commit transaction
  119. db.exec("COMMIT");
  120. console.log(`\n${c.green}✓ Migration completed successfully!${c.reset}`);
  121. // Show summary
  122. const collections = db.query<{ collection: string; count: number }, []>(`
  123. SELECT collection, COUNT(*) as count
  124. FROM documents
  125. WHERE active = 1
  126. GROUP BY collection
  127. ORDER BY collection
  128. `).all();
  129. console.log(`\n${c.dim}Documents by collection:${c.reset}`);
  130. for (const coll of collections) {
  131. console.log(` ${coll.collection}: ${coll.count} files`);
  132. }
  133. } catch (error) {
  134. db.exec("ROLLBACK");
  135. console.error(`\n${c.yellow}✗ Migration failed:${c.reset} ${error}`);
  136. console.error(`${c.dim}Database rolled back to previous state${c.reset}`);
  137. process.exit(1);
  138. } finally {
  139. db.close();
  140. }