diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 47f797d..e0e712b 100644 --- a/.beads/issues.jsonl +++ b/.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"} diff --git a/qmd.ts b/qmd.ts index 1a886b0..c09b434 100755 --- a/qmd.ts +++ b/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}`); diff --git a/store.ts b/store.ts index 2c8fca4..ed6e0c0 100644 --- a/store.ts +++ b/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 // =============================================================================