package storage import ( "database/sql" "fmt" "time" "donniemarko/internal/note" _ "modernc.org/sqlite" ) type SQLiteStorage struct { db *sql.DB } const sqliteSchema = ` PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS notes ( id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, title TEXT NOT NULL, content TEXT NOT NULL, updated_at INTEGER NOT NULL, size INTEGER NOT NULL, published INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS notes_updated_at_idx ON notes(updated_at); CREATE INDEX IF NOT EXISTS notes_path_idx ON notes(path); ` func NewSQLiteStorage(dbPath string) (*SQLiteStorage, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } if _, err := db.Exec(sqliteSchema); err != nil { _ = db.Close() return nil, fmt.Errorf("init schema: %w", err) } return &SQLiteStorage{db: db}, nil } func (s *SQLiteStorage) Close() error { if s.db == nil { return nil } return s.db.Close() } func (s *SQLiteStorage) GetAll() []*note.Note { rows, err := s.db.Query(` SELECT id, path, title, content, updated_at, size, published FROM notes `) if err != nil { return []*note.Note{} } defer rows.Close() var notes []*note.Note for rows.Next() { n, err := scanNote(rows) if err != nil { continue } notes = append(notes, n) } return notes } func (s *SQLiteStorage) Get(id string) (*note.Note, error) { row := s.db.QueryRow(` SELECT id, path, title, content, updated_at, size, published FROM notes WHERE id = ? `, id) n, err := scanNote(row) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("No note with id '%s'", id) } return nil, err } return n, nil } func (s *SQLiteStorage) Create(n *note.Note) error { _, err := s.db.Exec(` INSERT INTO notes (id, path, title, content, updated_at, size, published) VALUES (?, ?, ?, ?, ?, ?, ?) `, n.ID, n.Path, n.Title, n.Content, toUnix(n.UpdatedAt), n.Size, boolToInt(n.Published), ) if err != nil { return fmt.Errorf("insert note: %w", err) } return nil } func (s *SQLiteStorage) Delete(id string) { _, _ = s.db.Exec(`DELETE FROM notes WHERE id = ?`, id) } func (s *SQLiteStorage) Update(id string, n *note.Note) { _, _ = s.db.Exec(` UPDATE notes SET path = ?, title = ?, content = ?, updated_at = ?, size = ?, published = ? WHERE id = ? `, n.Path, n.Title, n.Content, toUnix(n.UpdatedAt), n.Size, boolToInt(n.Published), id, ) } func (s *SQLiteStorage) Search(query string) []*note.Note { pattern := "%" + query + "%" rows, err := s.db.Query(` SELECT id, path, title, content, updated_at, size, published FROM notes WHERE lower(content) LIKE lower(?) `, pattern) if err != nil { return []*note.Note{} } defer rows.Close() var notes []*note.Note for rows.Next() { n, err := scanNote(rows) if err != nil { continue } notes = append(notes, n) } return notes } func (s *SQLiteStorage) Count() int { row := s.db.QueryRow(`SELECT COUNT(*) FROM notes`) var count int if err := row.Scan(&count); err != nil { return 0 } return count } type scanner interface { Scan(dest ...interface{}) error } func scanNote(row scanner) (*note.Note, error) { var ( id string path string title string content string updated int64 size int64 published int ) if err := row.Scan(&id, &path, &title, &content, &updated, &size, &published); err != nil { return nil, err } return ¬e.Note{ ID: id, Path: path, Title: title, Content: content, UpdatedAt: fromUnix(updated), Size: size, Published: published != 0, }, nil } func toUnix(t time.Time) int64 { if t.IsZero() { return 0 } return t.UTC().Unix() } func fromUnix(v int64) time.Time { if v == 0 { return time.Time{} } return time.Unix(v, 0).UTC() } func boolToInt(v bool) int { if v { return 1 } return 0 }