feat: tag system
This commit is contained in:
@ -27,8 +27,23 @@ CREATE TABLE IF NOT EXISTS notes (
|
||||
published INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note_tags (
|
||||
note_id TEXT NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
UNIQUE(note_id, tag_id),
|
||||
FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS notes_updated_at_idx ON notes(updated_at);
|
||||
CREATE INDEX IF NOT EXISTS notes_path_idx ON notes(path);
|
||||
CREATE INDEX IF NOT EXISTS note_tags_note_idx ON note_tags(note_id);
|
||||
CREATE INDEX IF NOT EXISTS note_tags_tag_idx ON note_tags(tag_id);
|
||||
`
|
||||
|
||||
func NewSQLiteStorage(dbPath string) (*SQLiteStorage, error) {
|
||||
@ -68,6 +83,7 @@ func (s *SQLiteStorage) GetAll() []*note.Note {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
n.Tags = s.GetTags(n.ID)
|
||||
notes = append(notes, n)
|
||||
}
|
||||
|
||||
@ -89,6 +105,7 @@ func (s *SQLiteStorage) Get(id string) (*note.Note, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.Tags = s.GetTags(n.ID)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@ -150,6 +167,7 @@ func (s *SQLiteStorage) Search(query string) []*note.Note {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
n.Tags = s.GetTags(n.ID)
|
||||
notes = append(notes, n)
|
||||
}
|
||||
|
||||
@ -165,6 +183,94 @@ func (s *SQLiteStorage) Count() int {
|
||||
return count
|
||||
}
|
||||
|
||||
func (s *SQLiteStorage) AddTag(noteID, tag string) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT OR IGNORE INTO tags (name) VALUES (?)`, tag); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("insert tag: %w", err)
|
||||
}
|
||||
|
||||
var tagID int64
|
||||
if err := tx.QueryRow(`SELECT id FROM tags WHERE name = ?`, tag).Scan(&tagID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("lookup tag: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT OR IGNORE INTO note_tags (note_id, tag_id) VALUES (?, ?)`, noteID, tagID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("attach tag: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLiteStorage) RemoveTag(noteID, tag string) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
|
||||
var tagID int64
|
||||
err = tx.QueryRow(`SELECT id FROM tags WHERE name = ?`, tag).Scan(&tagID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
_ = tx.Rollback()
|
||||
return nil
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("lookup tag: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM note_tags WHERE note_id = ? AND tag_id = ?`, noteID, tagID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("detach tag: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM tags WHERE id = ? AND NOT EXISTS (SELECT 1 FROM note_tags WHERE tag_id = ?)`, tagID, tagID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("cleanup tag: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLiteStorage) GetTags(noteID string) []string {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT t.name
|
||||
FROM tags t
|
||||
JOIN note_tags nt ON nt.tag_id = t.id
|
||||
WHERE nt.note_id = ?
|
||||
ORDER BY t.name
|
||||
`, noteID)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tags []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
continue
|
||||
}
|
||||
tags = append(tags, name)
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
type scanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user