This commit is contained in:
nose
2025-12-12 21:55:38 -08:00
parent e2ffcab030
commit 85750247cc
78 changed files with 5726 additions and 6239 deletions

View File

@@ -231,11 +231,13 @@ class API_folder_store:
cursor.execute("""
CREATE TABLE IF NOT EXISTS notes (
hash TEXT PRIMARY KEY NOT NULL,
hash TEXT NOT NULL,
name TEXT NOT NULL,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (hash) REFERENCES files(hash) ON DELETE CASCADE
FOREIGN KEY (hash) REFERENCES files(hash) ON DELETE CASCADE,
PRIMARY KEY (hash, name)
)
""")
@@ -261,6 +263,11 @@ class API_folder_store:
cursor.execute("CREATE INDEX IF NOT EXISTS idx_worker_type ON worker(worker_type)")
self._migrate_metadata_schema(cursor)
self._migrate_notes_schema(cursor)
# Notes indices (after migration so columns exist)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notes_hash ON notes(hash)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notes_name ON notes(name)")
self.connection.commit()
logger.debug("Database tables created/verified")
@@ -448,6 +455,42 @@ class API_folder_store:
self.connection.commit()
except Exception as e:
logger.debug(f"Note: Schema import/migration completed with status: {e}")
def _migrate_notes_schema(self, cursor) -> None:
"""Migrate legacy notes schema (hash PRIMARY KEY, note) to named notes (hash,name PRIMARY KEY)."""
try:
cursor.execute("PRAGMA table_info(notes)")
cols = [row[1] for row in cursor.fetchall()]
if not cols:
return
if "name" in cols:
return
logger.info("Migrating legacy notes table to named notes schema")
cursor.execute("""
CREATE TABLE IF NOT EXISTS notes_new (
hash TEXT NOT NULL,
name TEXT NOT NULL,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (hash) REFERENCES files(hash) ON DELETE CASCADE,
PRIMARY KEY (hash, name)
)
""")
# Copy existing notes into the default key
cursor.execute("""
INSERT INTO notes_new (hash, name, note, created_at, updated_at)
SELECT hash, 'default', note, created_at, updated_at
FROM notes
""")
cursor.execute("DROP TABLE notes")
cursor.execute("ALTER TABLE notes_new RENAME TO notes")
self.connection.commit()
except Exception as exc:
logger.debug(f"Notes schema migration skipped/failed: {exc}")
def _update_metadata_modified_time(self, file_hash: str) -> None:
"""Update the time_modified timestamp for a file's metadata."""
@@ -1052,40 +1095,78 @@ class API_folder_store:
return []
def get_note(self, file_hash: str) -> Optional[str]:
"""Get note for a file by hash."""
"""Get the default note for a file by hash."""
try:
cursor = self.connection.cursor()
cursor.execute("""
SELECT n.note FROM notes n
WHERE n.hash = ?
""", (file_hash,))
row = cursor.fetchone()
return row[0] if row else None
notes = self.get_notes(file_hash)
if not notes:
return None
return notes.get("default")
except Exception as e:
logger.error(f"Error getting note for hash {file_hash}: {e}", exc_info=True)
return None
def get_notes(self, file_hash: str) -> Dict[str, str]:
"""Get all notes for a file by hash."""
try:
cursor = self.connection.cursor()
cursor.execute(
"SELECT name, note FROM notes WHERE hash = ? ORDER BY name ASC",
(file_hash,),
)
out: Dict[str, str] = {}
for name, note in cursor.fetchall() or []:
if not name:
continue
out[str(name)] = str(note or "")
return out
except Exception as e:
logger.error(f"Error getting notes for hash {file_hash}: {e}", exc_info=True)
return {}
def save_note(self, file_path: Path, note: str) -> None:
"""Save note for a file."""
"""Save the default note for a file."""
self.set_note(file_path, "default", note)
def set_note(self, file_path: Path, name: str, note: str) -> None:
"""Set a named note for a file."""
try:
note_name = str(name or "").strip()
if not note_name:
raise ValueError("Note name is required")
file_hash = self.get_or_create_file_entry(file_path)
cursor = self.connection.cursor()
cursor.execute("""
INSERT INTO notes (hash, note)
VALUES (?, ?)
ON CONFLICT(hash) DO UPDATE SET
cursor.execute(
"""
INSERT INTO notes (hash, name, note)
VALUES (?, ?, ?)
ON CONFLICT(hash, name) DO UPDATE SET
note = excluded.note,
updated_at = CURRENT_TIMESTAMP
""", (file_hash, note))
""",
(file_hash, note_name, note),
)
self.connection.commit()
logger.debug(f"Saved note for {file_path}")
logger.debug(f"Saved note '{note_name}' for {file_path}")
except Exception as e:
logger.error(f"Error saving note for {file_path}: {e}", exc_info=True)
raise
def delete_note(self, file_hash: str, name: str) -> None:
"""Delete a named note for a file by hash."""
try:
note_name = str(name or "").strip()
if not note_name:
raise ValueError("Note name is required")
cursor = self.connection.cursor()
cursor.execute(
"DELETE FROM notes WHERE hash = ? AND name = ?",
(file_hash, note_name),
)
self.connection.commit()
except Exception as e:
logger.error(f"Error deleting note '{name}' for hash {file_hash}: {e}", exc_info=True)
raise
def search_by_tag(self, tag: str, limit: int = 100) -> List[tuple]:
"""Search for files with a specific tag. Returns list of (hash, file_path) tuples."""
@@ -2027,7 +2108,7 @@ def migrate_tags_to_db(library_root: Path, db: API_folder_store) -> int:
try:
for tags_file in library_root.rglob("*.tag"):
try:
base_path = tags_file.with_suffix("")
base_path = tags_file.with_suffix("")
tags_text = tags_file.read_text(encoding='utf-8')
tags = [line.strip() for line in tags_text.splitlines() if line.strip()]