Proof of concept for listing notes

pull/6/head
Mickaël Menu 3 years ago
parent 16e7b55d41
commit 2cd15aafe2
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -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
}

@ -28,7 +28,7 @@ func (cmd *Index) Run(container *Container) error {
}
return db.WithTransaction(func(tx sqlite.Transaction) error {
indexer, err := sqlite.NewNoteIndexer(tx, zk.Path, container.Logger)
indexer, err := sqlite.NewNoteDAO(tx, zk.Path, container.Logger)
if err != nil {
return err
}

@ -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() {}

@ -11,6 +11,7 @@ var Build = "dev"
var cli struct {
Index cmd.Index `cmd help:"Index the notes in the given directory to be searchable"`
Init cmd.Init `cmd help:"Create a slip box in the given directory"`
List cmd.List `cmd help:"List notes matching given criteria"`
New cmd.New `cmd help:"Create a new note in the given slip box directory"`
Version kong.VersionFlag `help:"Print zk version"`
}

Loading…
Cancel
Save