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>
This commit is contained in:
Tobi Lutke 2025-12-12 16:34:52 -05:00
parent 6470497f2c
commit 094fa64bee
No known key found for this signature in database
3 changed files with 53 additions and 49 deletions

View File

@ -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"}

59
qmd.ts
View File

@ -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}`);

View File

@ -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
// =============================================================================