mirror of https://github.com/mickael-menu/zk
Proof of concept for listing notes
parent
16e7b55d41
commit
2cd15aafe2
@ -1,129 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mickael-menu/zk/core/file"
|
||||
"github.com/mickael-menu/zk/core/note"
|
||||
"github.com/mickael-menu/zk/util"
|
||||
)
|
||||
|
||||
// NoteIndexer retrieves and stores notes indexation in the SQLite database.
|
||||
// It implements the Core port note.Indexer.
|
||||
type NoteIndexer struct {
|
||||
tx Transaction
|
||||
root string
|
||||
logger util.Logger
|
||||
|
||||
// Prepared SQL statements
|
||||
indexedStmt *sql.Stmt
|
||||
addStmt *sql.Stmt
|
||||
updateStmt *sql.Stmt
|
||||
removeStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func NewNoteIndexer(tx Transaction, root string, logger util.Logger) (*NoteIndexer, error) {
|
||||
indexedStmt, err := tx.Prepare(`
|
||||
SELECT filename, dir, modified from notes
|
||||
ORDER BY dir, filename ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addStmt, err := tx.Prepare(`
|
||||
INSERT INTO notes (filename, dir, title, body, word_count, checksum, created, modified)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateStmt, err := tx.Prepare(`
|
||||
UPDATE notes
|
||||
SET title = ?, body = ?, word_count = ?, checksum = ?, modified = ?
|
||||
WHERE filename = ? AND dir = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeStmt, err := tx.Prepare(`
|
||||
DELETE FROM notes
|
||||
WHERE filename = ? AND dir = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NoteIndexer{
|
||||
tx: tx,
|
||||
root: root,
|
||||
logger: logger,
|
||||
indexedStmt: indexedStmt,
|
||||
addStmt: addStmt,
|
||||
updateStmt: updateStmt,
|
||||
removeStmt: removeStmt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *NoteIndexer) Indexed() (<-chan file.Metadata, error) {
|
||||
rows, err := i.indexedStmt.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := make(chan file.Metadata)
|
||||
go func() {
|
||||
defer close(c)
|
||||
defer rows.Close()
|
||||
var (
|
||||
filename, dir string
|
||||
modified time.Time
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&filename, &dir, &modified)
|
||||
if err != nil {
|
||||
i.logger.Err(err)
|
||||
}
|
||||
|
||||
c <- file.Metadata{
|
||||
Path: file.Path{Dir: dir, Filename: filename, Abs: filepath.Join(i.root, dir, filename)},
|
||||
Modified: modified,
|
||||
}
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
i.logger.Err(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (i *NoteIndexer) Add(metadata note.Metadata) error {
|
||||
_, err := i.addStmt.Exec(
|
||||
metadata.Path.Filename, metadata.Path.Dir, metadata.Title,
|
||||
metadata.Body, metadata.WordCount, metadata.Checksum,
|
||||
metadata.Created, metadata.Modified,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *NoteIndexer) Update(metadata note.Metadata) error {
|
||||
_, err := i.updateStmt.Exec(
|
||||
metadata.Title, metadata.Body, metadata.WordCount,
|
||||
metadata.Checksum, metadata.Modified,
|
||||
metadata.Path.Filename, metadata.Path.Dir,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *NoteIndexer) Remove(path file.Path) error {
|
||||
_, err := i.updateStmt.Exec(path.Filename, path.Dir)
|
||||
return err
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mickael-menu/zk/core/file"
|
||||
"github.com/mickael-menu/zk/core/note"
|
||||
"github.com/mickael-menu/zk/util"
|
||||
)
|
||||
|
||||
// NoteDAO persists notes in the SQLite database.
|
||||
// It implements the core ports note.Indexer and note.Finder.
|
||||
type NoteDAO struct {
|
||||
tx Transaction
|
||||
root string
|
||||
logger util.Logger
|
||||
|
||||
// Prepared SQL statements
|
||||
indexedStmt *sql.Stmt
|
||||
addStmt *sql.Stmt
|
||||
updateStmt *sql.Stmt
|
||||
removeStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func NewNoteDAO(tx Transaction, root string, logger util.Logger) (*NoteDAO, error) {
|
||||
indexedStmt, err := tx.Prepare(`
|
||||
SELECT dir, filename, modified from notes
|
||||
ORDER BY dir, filename ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addStmt, err := tx.Prepare(`
|
||||
INSERT INTO notes (dir, filename, title, body, word_count, checksum, created, modified)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateStmt, err := tx.Prepare(`
|
||||
UPDATE notes
|
||||
SET title = ?, body = ?, word_count = ?, checksum = ?, modified = ?
|
||||
WHERE dir = ? AND filename = ?`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeStmt, err := tx.Prepare(`
|
||||
DELETE FROM notes
|
||||
WHERE dir = ? AND filename = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NoteDAO{
|
||||
tx: tx,
|
||||
root: root,
|
||||
logger: logger,
|
||||
indexedStmt: indexedStmt,
|
||||
addStmt: addStmt,
|
||||
updateStmt: updateStmt,
|
||||
removeStmt: removeStmt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *NoteDAO) Indexed() (<-chan file.Metadata, error) {
|
||||
rows, err := d.indexedStmt.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := make(chan file.Metadata)
|
||||
go func() {
|
||||
defer close(c)
|
||||
defer rows.Close()
|
||||
var (
|
||||
dir, filename string
|
||||
modified time.Time
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&dir, &filename, &modified)
|
||||
if err != nil {
|
||||
d.logger.Err(err)
|
||||
}
|
||||
|
||||
c <- file.Metadata{
|
||||
Path: file.Path{Dir: dir, Filename: filename, Abs: filepath.Join(d.root, dir, filename)},
|
||||
Modified: modified,
|
||||
}
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
d.logger.Err(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (d *NoteDAO) Add(note note.Metadata) error {
|
||||
_, err := d.addStmt.Exec(
|
||||
note.Path.Dir, note.Path.Filename, note.Title,
|
||||
note.Body, note.WordCount, note.Checksum,
|
||||
note.Created, note.Modified,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *NoteDAO) Update(note note.Metadata) error {
|
||||
_, err := d.updateStmt.Exec(
|
||||
note.Title, note.Body, note.WordCount,
|
||||
note.Checksum, note.Modified,
|
||||
note.Path.Dir, note.Path.Filename,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *NoteDAO) Remove(path file.Path) error {
|
||||
_, err := d.updateStmt.Exec(path.Dir, path.Filename)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *NoteDAO) Find(callback func(note.Match) error, filters ...note.Filter) error {
|
||||
rows, err := func() (*sql.Rows, error) {
|
||||
if len(filters) == 0 {
|
||||
return d.tx.Query(`
|
||||
SELECT id, dir, filename, title, body, word_count, created, modified,
|
||||
checksum, "" as snippet from notes
|
||||
ORDER BY title ASC
|
||||
`)
|
||||
} else {
|
||||
filter := filters[0].(note.QueryFilter)
|
||||
return d.tx.Query(`
|
||||
SELECT n.id, n.dir, n.filename, n.title, n.body, n.word_count,
|
||||
n.created, n.modified, n.checksum,
|
||||
snippet(notes_fts, -1, '\033[31m', '\033[0m', '…', 20) as snippet
|
||||
FROM notes n
|
||||
JOIN notes_fts
|
||||
ON n.id = notes_fts.rowid
|
||||
WHERE notes_fts MATCH ?
|
||||
ORDER BY bm25(notes_fts, 1000.0, 1.0)
|
||||
--- ORDER BY rank
|
||||
`, filter)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, wordCount int
|
||||
title, body, snippet string
|
||||
dir, filename, checksum string
|
||||
created, modified time.Time
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &dir, &filename, &title, &body, &wordCount, &created, &modified, &checksum, &snippet)
|
||||
if err != nil {
|
||||
d.logger.Err(err)
|
||||
continue
|
||||
}
|
||||
|
||||
callback(note.Match{
|
||||
ID: id,
|
||||
Snippet: snippet,
|
||||
Metadata: note.Metadata{
|
||||
Path: file.Path{
|
||||
Dir: dir,
|
||||
Filename: filename,
|
||||
Abs: filepath.Join(d.root, dir, filename),
|
||||
},
|
||||
Title: title,
|
||||
Body: body,
|
||||
WordCount: wordCount,
|
||||
Created: created,
|
||||
Modified: modified,
|
||||
Checksum: checksum,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/mickael-menu/zk/adapter/sqlite"
|
||||
"github.com/mickael-menu/zk/core/note"
|
||||
"github.com/mickael-menu/zk/core/zk"
|
||||
)
|
||||
|
||||
// List displays notes matching a set of criteria.
|
||||
type List struct {
|
||||
Query string `arg optional help:"Terms to search for in the notes" placeholder:"TERMS"`
|
||||
}
|
||||
|
||||
func (cmd *List) Run(container *Container) error {
|
||||
zk, err := zk.Open(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := container.Database(zk.DBPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.WithTransaction(func(tx sqlite.Transaction) error {
|
||||
notes, err := sqlite.NewNoteDAO(tx, zk.Path, container.Logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters := make([]note.Filter, 0)
|
||||
if cmd.Query != "" {
|
||||
filters = append(filters, note.QueryFilter(cmd.Query))
|
||||
}
|
||||
|
||||
return note.List(notes, filters...)
|
||||
})
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package note
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QueryFilter string
|
||||
|
||||
type Match struct {
|
||||
ID int
|
||||
Snippet string
|
||||
Metadata
|
||||
}
|
||||
|
||||
type Finder interface {
|
||||
Find(callback func(Match) error, filters ...Filter) error
|
||||
}
|
||||
|
||||
func List(finder Finder, filters ...Filter) error {
|
||||
return finder.Find(func(note Match) error {
|
||||
fmt.Printf("%v\n", strings.ReplaceAll(note.Snippet, "\\033", "\033"))
|
||||
return nil
|
||||
}, filters...)
|
||||
}
|
||||
|
||||
// Filter is a sealed interface implemented by Finder filter criteria.
|
||||
type Filter interface{ sealed() }
|
||||
|
||||
func (f QueryFilter) sealed() {}
|
Loading…
Reference in New Issue