feat: sqlite storage draft
This commit is contained in:
217
internal/storage/sqlite.go
Normal file
217
internal/storage/sqlite.go
Normal file
@ -0,0 +1,217 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user