Refactor generation of the SQL find query and add support for PathsFilter

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

@ -51,3 +51,12 @@
checksum: "iecywst"
created: "2019-11-20T20:32:56+01:00"
modified: "2019-11-20T20:34:06+01:00"
- id: 7
path: "log/2021-02-04.md"
title: "February 4, 2021"
body: "A third daily note"
word_count: 4
checksum: "earkte"
created: "2020-11-29T08:20:18+01:00"
modified: "2020-11-29T08:20:18+01:00"

@ -2,6 +2,8 @@ package sqlite
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/mickael-menu/zk/core/note"
@ -146,26 +148,51 @@ func (d *NoteDAO) exists(path string) (bool, error) {
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, path, title, body, word_count, created, modified,
checksum, "" as snippet from notes
ORDER BY title ASC
`)
} else {
filter := filters[0].(note.MatchFilter)
return d.tx.Query(`
SELECT n.id, n.path, n.title, n.body, n.word_count,
n.created, n.modified, n.checksum,
snippet(notes_fts, -1, '<zk:match>', '</zk:match>', '…', 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, 500.0, 1.0)
--- ORDER BY rank
`, fts5.ConvertQuery(string(filter)))
snippetCol := `""`
orderTerm := `n.title ASC`
whereExprs := make([]string, 0)
args := make([]interface{}, 0)
for _, filter := range filters {
switch filter := filter.(type) {
case note.MatchFilter:
snippetCol = `snippet(notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20) as snippet`
orderTerm = `bm25(notes_fts, 1000.0, 500.0, 1.0)`
whereExprs = append(whereExprs, "notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(string(filter)))
case note.PathFilter:
if len(filter) == 0 {
break
}
globs := make([]string, 0)
for _, path := range filter {
globs = append(globs, "n.path GLOB ?")
args = append(args, path+"*")
}
whereExprs = append(whereExprs, strings.Join(globs, " OR "))
default:
panic("unknown filter type")
}
}
query := "SELECT n.id, n.path, n.title, n.body, n.word_count, n.created, n.modified, n.checksum, " + snippetCol
query += `
FROM notes n
JOIN notes_fts
ON n.id = notes_fts.rowid
`
if len(whereExprs) > 0 {
query += " WHERE " + strings.Join(whereExprs, " AND ")
}
query += " ORDER BY " + orderTerm
return d.tx.Query(query, args...)
}()
if err != nil {

@ -17,38 +17,42 @@ func TestNoteDAOIndexed(t *testing.T) {
expected := []paths.Metadata{
{
Path: "f39c8.md",
Modified: test.Date("2020-01-20T08:52:42+01:00"),
Modified: time.Date(2020, 1, 20, 8, 52, 42, 0, time.Local),
},
{
Path: "index.md",
Modified: test.Date("2019-12-04T12:17:21+01:00"),
Modified: time.Date(2019, 12, 4, 12, 17, 21, 0, time.Local),
},
{
Path: "log/2021-01-03.md",
Modified: test.Date("2020-11-22T16:27:45+01:00"),
Modified: time.Date(2020, 11, 22, 16, 27, 45, 0, time.Local),
},
{
Path: "log/2021-01-04.md",
Modified: test.Date("2020-11-29T08:20:18+01:00"),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
},
{
Path: "log/2021-02-04.md",
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
},
{
Path: "ref/test/a.md",
Modified: test.Date("2019-11-20T20:34:06+01:00"),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.Local),
},
{
Path: "ref/test/b.md",
Modified: test.Date("2019-11-20T20:34:06+01:00"),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.Local),
},
}
c, err := dao.Indexed()
assert.Nil(t, err)
i := 0
for item := range c {
assert.Equal(t, item, expected[i])
i++
actual := make([]paths.Metadata, 0)
for a := range c {
actual = append(actual, a)
}
assert.Equal(t, actual, expected)
})
}
@ -175,6 +179,18 @@ func TestNoteDAOFindAll(t *testing.T) {
Checksum: "iecywst",
},
},
{
Snippet: "",
Metadata: note.Metadata{
Path: "log/2021-02-04.md",
Title: "February 4, 2021",
Body: "A third daily note",
WordCount: 4,
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Checksum: "earkte",
},
},
{
Snippet: "",
Metadata: note.Metadata{
@ -216,6 +232,18 @@ func TestNoteDAOFindAll(t *testing.T) {
func TestNoteDAOFindMatch(t *testing.T) {
expected := []note.Match{
{
Snippet: "<zk:match>Index</zk:match> of the Zettelkasten",
Metadata: note.Metadata{
Path: "index.md",
Title: "Index",
Body: "Index of the Zettelkasten",
WordCount: 4,
Created: time.Date(2019, 12, 4, 11, 59, 11, 0, time.Local),
Modified: time.Date(2019, 12, 4, 12, 17, 21, 0, time.Local),
Checksum: "iaefhv",
},
},
{
Snippet: "A <zk:match>daily</zk:match> note",
Metadata: note.Metadata{
@ -240,9 +268,95 @@ func TestNoteDAOFindMatch(t *testing.T) {
Checksum: "arstde",
},
},
{
Snippet: "A third <zk:match>daily</zk:match> note",
Metadata: note.Metadata{
Path: "log/2021-02-04.md",
Title: "February 4, 2021",
Body: "A third daily note",
WordCount: 4,
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Checksum: "earkte",
},
},
}
testNoteDAOFind(t, expected, note.MatchFilter("daily | index"))
}
func TestNoteDAOFindInPath(t *testing.T) {
expected := []note.Match{
{
Snippet: "",
Metadata: note.Metadata{
Path: "log/2021-01-03.md",
Title: "January 3, 2021",
Body: "A daily note",
WordCount: 3,
Created: time.Date(2020, 11, 22, 16, 27, 45, 0, time.Local),
Modified: time.Date(2020, 11, 22, 16, 27, 45, 0, time.Local),
Checksum: "qwfpgj",
},
},
{
Snippet: "",
Metadata: note.Metadata{
Path: "log/2021-01-04.md",
Title: "January 4, 2021",
Body: "A second daily note",
WordCount: 4,
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.Local),
Checksum: "arstde",
},
},
}
testNoteDAOFind(t, expected, note.PathFilter([]string{"log/2021-01-*"}))
}
func TestNoteDAOFindInMultiplePath(t *testing.T) {
expected := []note.Match{
{
Snippet: "",
Metadata: note.Metadata{
Path: "ref/test/b.md",
Title: "A nested note",
Body: "This one is in a sub sub directory",
WordCount: 8,
Created: time.Date(2019, 11, 20, 20, 32, 56, 0, time.Local),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.Local),
Checksum: "yvwbae",
},
},
{
Snippet: "",
Metadata: note.Metadata{
Path: "ref/test/a.md",
Title: "Another nested note",
Body: "It shall appear before b.md",
WordCount: 5,
Created: time.Date(2019, 11, 20, 20, 32, 56, 0, time.Local),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.Local),
Checksum: "iecywst",
},
},
{
Snippet: "",
Metadata: note.Metadata{
Path: "index.md",
Title: "Index",
Body: "Index of the Zettelkasten",
WordCount: 4,
Created: time.Date(2019, 12, 4, 11, 59, 11, 0, time.Local),
Modified: time.Date(2019, 12, 4, 12, 17, 21, 0, time.Local),
Checksum: "iaefhv",
},
},
}
testNoteDAOFind(t, expected, note.MatchFilter("daily"))
testNoteDAOFind(t, expected, note.PathFilter([]string{"ref", "index.md"}))
}
func testNoteDAOFind(t *testing.T, expected []note.Match, filters ...note.Filter) {

@ -11,7 +11,7 @@ import (
// List displays notes matching a set of criteria.
type List struct {
Path []string `arg optional placeholder:"PATHS"`
Paths []string `arg optional placeholder:"PATHS"`
Match string `help:"Terms to search for in the notes" placeholder:"TERMS"`
Format string `help:"Pretty prints the list using the given format" placeholder:"TEMPLATE"`
}
@ -31,6 +31,18 @@ func (cmd *List) Run(container *Container) error {
notes := sqlite.NewNoteDAO(tx, container.Logger)
filters := make([]note.Filter, 0)
paths := make([]string, 0)
for _, p := range cmd.Paths {
dir, err := zk.DirAt(p)
if err == nil {
paths = append(paths, dir.Name)
}
}
if len(paths) > 0 {
filters = append(filters, note.PathFilter(paths))
}
if cmd.Match != "" {
filters = append(filters, note.MatchFilter(cmd.Match))
}

@ -15,6 +15,9 @@ import (
// MatchFilter is a note filter used to match its content with FTS predicates.
type MatchFilter string
// PathFilter is a note filter using path globs to match notes.
type PathFilter []string
// Match holds information about a note matching the list filters.
type Match struct {
// Snippet is an excerpt of the note.
@ -156,3 +159,4 @@ type matchRenderContext struct {
type Filter interface{ sealed() }
func (f MatchFilter) sealed() {}
func (f PathFilter) sealed() {}

Loading…
Cancel
Save