Răsfoiți Sursa

Refactor: Move collection DB operations to store.ts

- Added listCollections(), removeCollection(), renameCollection() to store.ts
- Updated qmd.ts to use store methods instead of direct DB operations
- Removed ~15 direct DB calls from qmd.ts collection functions
- All 9 collection command tests passing

Part of larger refactoring to move all DB operations to store.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Tobi Lutke 5 luni în urmă
părinte
comite
094fa64bee
3 a modificat fișierele cu 53 adăugiri și 49 ștergeri
  1. 1 0
      .beads/issues.jsonl
  2. 10 49
      qmd.ts
  3. 42 0
      store.ts

+ 1 - 0
.beads/issues.jsonl

@@ -1,3 +1,4 @@
+{"id":"qmd-29c","title":"Move all database operations from qmd.ts to store.ts","description":"Currently qmd.ts has ~70 direct database operations (db.prepare, db.exec). All database operations should be moved to store.ts to improve separation of concerns. qmd.ts should only use high-level methods from store.ts that don't require direct SQL knowledge.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-12-12T16:32:13.722223-05:00","updated_at":"2025-12-12T16:32:25.423644-05:00"}
 {"id":"qmd-4ru","title":"Update document retrieval for new schema","description":"Functions like getDocument, findDocument, getMultipleDocuments need to work with new schema (path instead of filepath, content joins, virtual paths).","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.911881-05:00","updated_at":"2025-12-12T15:56:11.054888-05:00","closed_at":"2025-12-12T15:56:11.054888-05:00","dependencies":[{"issue_id":"qmd-4ru","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.912607-05:00","created_by":"daemon"}]}
 {"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T16:29:24.153196-05:00","closed_at":"2025-12-12T16:29:24.153196-05:00"}
 {"id":"qmd-ama","title":"Refactor database system","description":"All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection, path, hash,\n┃ created_at, updated_at. (collection,path)\n┃\n┃\n\n┃ All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection_id, path, hash,\n┃ created_at, updated_at. (collection,path) is unique. There is also collection which stores PWD\n┃ + glob pattern, name (\\w+). Every document is treated as path qmd://collection.name/","notes":"## Completed\n- ✅ Implemented content-addressable storage (content table with hash→doc mapping)\n- ✅ Refactored documents table as file system layer (collection_id, path, hash)\n- ✅ Added collection names (e.g., \"pages\", \"journals\", \"archive\")\n- ✅ Implemented virtual paths (qmd://collection-name/path/to/file.md)\n- ✅ Added hierarchical context support (collection-scoped)\n- ✅ Successfully migrated existing database\n- ✅ Updated search functions to work with new schema\n- ✅ Updated indexing logic to use content-addressable storage\n- ✅ Orphaned content hash cleanup\n\n## Still TODO\n- Fix migration SQL to properly extract basename (currently needs manual fix)\n- Implement `qmd collection add . --name \u003cname\u003e --mask '**/*.md'`\n- Implement `qmd ls [path]` for exploring virtual file tree","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:35.497489-05:00","updated_at":"2025-12-12T15:39:48.879143-05:00","closed_at":"2025-12-12T15:39:48.879143-05:00"}

+ 10 - 49
qmd.ts

@@ -20,6 +20,9 @@ import {
   getContextForPath,
   getCollectionIdByName,
   getCollectionByName,
+  listCollections,
+  removeCollection,
+  renameCollection,
   findSimilarFiles,
   matchFilesByGlob,
   getHashesNeedingEmbedding,
@@ -1250,29 +1253,7 @@ function listFiles(pathArg?: string): void {
 // Collection management commands
 function collectionList(): void {
   const db = getDb();
-
-  const collections = db.prepare(`
-    SELECT
-      c.id,
-      c.name,
-      c.pwd,
-      c.glob_pattern,
-      c.created_at,
-      c.updated_at,
-      COUNT(d.id) as file_count
-    FROM collections c
-    LEFT JOIN documents d ON d.collection_id = c.id AND d.active = 1
-    GROUP BY c.id
-    ORDER BY c.name
-  `).all() as {
-    id: number;
-    name: string;
-    pwd: string;
-    glob_pattern: string;
-    created_at: string;
-    updated_at: string;
-    file_count: number;
-  }[];
+  const collections = listCollections(db);
 
   if (collections.length === 0) {
     console.log("No collections found. Run 'qmd add .' to create one.");
@@ -1289,7 +1270,7 @@ function collectionList(): void {
     console.log(`${c.cyan}${coll.name}${c.reset}`);
     console.log(`  ${c.dim}Path:${c.reset}     ${coll.pwd}`);
     console.log(`  ${c.dim}Pattern:${c.reset}  ${coll.glob_pattern}`);
-    console.log(`  ${c.dim}Files:${c.reset}    ${coll.file_count}`);
+    console.log(`  ${c.dim}Files:${c.reset}    ${coll.active_count}`);
     console.log(`  ${c.dim}Updated:${c.reset}  ${timeAgo}`);
     console.log();
   }
@@ -1349,30 +1330,12 @@ function collectionRemove(name: string): void {
     process.exit(1);
   }
 
-  // Get file count
-  const fileCount = db.prepare(`
-    SELECT COUNT(*) as count FROM documents WHERE collection_id = ? AND active = 1
-  `).get(coll.id) as { count: number };
-
-  // Delete documents
-  db.prepare(`DELETE FROM documents WHERE collection_id = ?`).run(coll.id);
-
-  // Delete contexts
-  db.prepare(`DELETE FROM path_contexts WHERE collection_id = ?`).run(coll.id);
-
-  // Delete collection
-  db.prepare(`DELETE FROM collections WHERE id = ?`).run(coll.id);
-
-  // Clean up orphaned content hashes
-  const cleanupResult = db.prepare(`
-    DELETE FROM content
-    WHERE hash NOT IN (SELECT DISTINCT hash FROM documents WHERE active = 1)
-  `).run();
+  const result = removeCollection(db, coll.id);
 
   console.log(`${c.green}✓${c.reset} Removed collection '${name}'`);
-  console.log(`  Deleted ${fileCount.count} documents`);
-  if (cleanupResult.changes > 0) {
-    console.log(`  Cleaned up ${cleanupResult.changes} orphaned content hashes`);
+  console.log(`  Deleted ${result.deletedDocs} documents`);
+  if (result.cleanedHashes > 0) {
+    console.log(`  Cleaned up ${result.cleanedHashes} orphaned content hashes`);
   }
 
   closeDb();
@@ -1399,9 +1362,7 @@ function collectionRename(oldName: string, newName: string): void {
     process.exit(1);
   }
 
-  // Update the collection name
-  db.prepare(`UPDATE collections SET name = ?, updated_at = ? WHERE id = ?`)
-    .run(newName, new Date().toISOString(), coll.id);
+  renameCollection(db, coll.id, newName);
 
   console.log(`${c.green}✓${c.reset} Renamed collection '${oldName}' to '${newName}'`);
   console.log(`  Virtual paths updated: ${c.cyan}qmd://${oldName}/${c.reset} → ${c.cyan}qmd://${newName}/${c.reset}`);

+ 42 - 0
store.ts

@@ -1076,6 +1076,48 @@ export function getCollectionByName(db: Database, name: string): { id: number; n
   return result;
 }
 
+export function listCollections(db: Database): { id: number; name: string; pwd: string; glob_pattern: string; created_at: string; updated_at: string; doc_count: number; active_count: number; last_modified: string | null }[] {
+  const collections = db.prepare(`
+    SELECT c.id, c.name, c.pwd, c.glob_pattern, c.created_at, c.updated_at,
+           COUNT(d.id) as doc_count,
+           SUM(CASE WHEN d.active = 1 THEN 1 ELSE 0 END) as active_count,
+           MAX(d.modified_at) as last_modified
+    FROM collections c
+    LEFT JOIN documents d ON d.collection_id = c.id
+    GROUP BY c.id
+    ORDER BY c.name
+  `).all() as { id: number; name: string; pwd: string; glob_pattern: string; created_at: string; updated_at: string; doc_count: number; active_count: number; last_modified: string | null }[];
+  return collections;
+}
+
+export function removeCollection(db: Database, collectionId: number): { deletedDocs: number; cleanedHashes: number } {
+  // Delete documents
+  const docResult = db.prepare(`DELETE FROM documents WHERE collection_id = ?`).run(collectionId);
+
+  // Delete contexts
+  db.prepare(`DELETE FROM path_contexts WHERE collection_id = ?`).run(collectionId);
+
+  // Delete collection
+  db.prepare(`DELETE FROM collections WHERE id = ?`).run(collectionId);
+
+  // Clean up orphaned content hashes
+  const cleanupResult = db.prepare(`
+    DELETE FROM content
+    WHERE hash NOT IN (SELECT DISTINCT hash FROM documents WHERE active = 1)
+  `).run();
+
+  return {
+    deletedDocs: docResult.changes,
+    cleanedHashes: cleanupResult.changes
+  };
+}
+
+export function renameCollection(db: Database, collectionId: number, newName: string): void {
+  const now = new Date().toISOString();
+  db.prepare(`UPDATE collections SET name = ?, updated_at = ? WHERE id = ?`)
+    .run(newName, now, collectionId);
+}
+
 // =============================================================================
 // FTS Search
 // =============================================================================