Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command to produce a graph of the indexed notes #106

Merged
merged 3 commits into from
Nov 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.

### Added

* New `zk graph --format json` command which produces a JSON graph of the notes matching the given criteria.
* New template variables `filename` and `filename-stem` when formatting notes (e.g. with `zk list --format`) and for the [`fzf-line`](docs/tool-fzf.md) config key.
* Customize how LSP completion items appear in your editor when auto-completing links with the [`[lsp.completion]` configuration section](docs/config-lsp.md).
```toml
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* [Creating notes from templates](docs/note-creation.md)
* [Advanced search and filtering capabilities](docs/note-filtering.md) including [tags](docs/tags.md), links and mentions
* [Integration with your favorite editors](docs/editors-integration.md):
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* [Any LSP-compatible editor](docs/editors-integration.md)
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* (*unmaintained*) [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+ by [Seth Messer](https://github.com/megalithic)
* [Interactive browser](docs/tool-fzf.md), powered by `fzf`
* [Git-style command aliases](docs/config-alias.md) and [named filters](docs/config-filter.md)
* [Made with automation in mind](docs/automation.md)
Expand Down
11 changes: 11 additions & 0 deletions internal/adapter/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ func (db *DB) migrate() error {
},
NeedsReindexing: true,
},

{ // 6
SQL: []string{
// View of links with the source and target notes metadata, for simpler queries.
`CREATE VIEW resolved_links AS
SELECT l.*, s.path AS source_path, s.title AS source_title, t.path AS target_path, t.title AS target_title
FROM links l
LEFT JOIN notes s ON l.source_id = s.id
LEFT JOIN notes t ON l.target_id = t.id`,
},
},
}

needsReindexing := false
Expand Down
2 changes: 1 addition & 1 deletion internal/adapter/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestMigrateFrom0(t *testing.T) {
var version int
err := tx.QueryRow("PRAGMA user_version").Scan(&version)
assert.Nil(t, err)
assert.Equal(t, version, 5)
assert.Equal(t, version, 6)

_, err = tx.Exec(`
INSERT INTO notes (path, sortable_path, title, body, word_count, checksum)
Expand Down
156 changes: 156 additions & 0 deletions internal/adapter/sqlite/link_dao.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package sqlite

import (
"database/sql"
"fmt"

"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
)

// LinkDAO persists links in the SQLite database.
type LinkDAO struct {
tx Transaction
logger util.Logger

// Prepared SQL statements
addLinkStmt *LazyStmt
setLinksTargetStmt *LazyStmt
removeLinksStmt *LazyStmt
}

// NewLinkDAO creates a new instance of a DAO working on the given database
// transaction.
func NewLinkDAO(tx Transaction, logger util.Logger) *LinkDAO {
return &LinkDAO{
tx: tx,
logger: logger,

// Add a new link.
addLinkStmt: tx.PrepareLazy(`
INSERT INTO links (source_id, target_id, title, href, type, external, rels, snippet, snippet_start, snippet_end)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),

// Set links matching a given href and missing a target ID to the given
// target ID.
setLinksTargetStmt: tx.PrepareLazy(`
UPDATE links
SET target_id = ?
WHERE target_id IS NULL AND external = 0 AND ? LIKE href || '%'
`),

// Remove all the outbound links of a note.
removeLinksStmt: tx.PrepareLazy(`
DELETE FROM links
WHERE source_id = ?
`),
}
}

// Add inserts all the outbound links of the given note.
func (d *LinkDAO) Add(links []core.ResolvedLink) error {
for _, link := range links {
sourceID := noteIDToSQL(link.SourceID)
targetID := noteIDToSQL(link.TargetID)

_, err := d.addLinkStmt.Exec(sourceID, targetID, link.Title, link.Href, link.Type, link.IsExternal, joinLinkRels(link.Rels), link.Snippet, link.SnippetStart, link.SnippetEnd)
if err != nil {
return err
}
}

return nil
}

// RemoveAll removes all the outbound links of the given note.
func (d *LinkDAO) RemoveAll(id core.NoteID) error {
_, err := d.removeLinksStmt.Exec(noteIDToSQL(id))
return err
}

// SetTargetID updates the missing target_id for links matching the given href.
// FIXME: Probably doesn't work for all type of href (partial, wikilinks, etc.)
func (d *LinkDAO) SetTargetID(href string, id core.NoteID) error {
_, err := d.setLinksTargetStmt.Exec(int64(id), href)
return err
}

// joinLinkRels will concatenate a list of rels into a SQLite ready string.
// Each rel is delimited by \x01 for easy matching in queries.
func joinLinkRels(rels []core.LinkRelation) string {
if len(rels) == 0 {
return ""
}
delimiter := "\x01"
res := delimiter
for _, rel := range rels {
res += string(rel) + delimiter
}
return res
}

func (d *LinkDAO) FindBetweenNotes(ids []core.NoteID) ([]core.ResolvedLink, error) {
links := make([]core.ResolvedLink, 0)

idsString := joinNoteIDs(ids, ",")
rows, err := d.tx.Query(fmt.Sprintf(`
SELECT id, source_id, source_path, target_id, target_path, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM resolved_links
WHERE source_id IN (%s) AND target_id IN (%s)
`, idsString, idsString))
if err != nil {
return links, err
}
defer rows.Close()

for rows.Next() {
link, err := d.scanLink(rows)
if err != nil {
d.logger.Err(err)
continue
}
if link != nil {
links = append(links, *link)
}
}

return links, nil
}

func (d *LinkDAO) scanLink(row RowScanner) (*core.ResolvedLink, error) {
var (
id, sourceID, targetID, snippetStart, snippetEnd int
sourcePath, targetPath, title, href, linkType, snippet string
external bool
rels sql.NullString
)

err := row.Scan(
&id, &sourceID, &sourcePath, &targetID, &targetPath, &title, &href,
&linkType, &external, &rels, &snippet, &snippetStart, &snippetEnd,
)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
return &core.ResolvedLink{
SourceID: core.NoteID(sourceID),
SourcePath: sourcePath,
TargetID: core.NoteID(targetID),
TargetPath: targetPath,
Link: core.Link{
Title: title,
Href: href,
Type: core.LinkType(linkType),
IsExternal: external,
Rels: core.LinkRels(parseListFromNullString(rels)...),
Snippet: snippet,
SnippetStart: snippetStart,
SnippetEnd: snippetEnd,
},
}, nil
}
}
53 changes: 53 additions & 0 deletions internal/adapter/sqlite/link_dao_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package sqlite

import (
"fmt"
"testing"

"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
)

func testLinkDAO(t *testing.T, callback func(tx Transaction, dao *LinkDAO)) {
testTransaction(t, func(tx Transaction) {
callback(tx, NewLinkDAO(tx, &util.NullLogger))
})
}

type linkRow struct {
SourceId core.NoteID
TargetId *core.NoteID
Href, Type, Title, Rels, Snippet string
SnippetStart, SnippetEnd int
IsExternal bool
}

func queryLinkRows(t *testing.T, q RowQuerier, where string) []linkRow {
links := make([]linkRow, 0)

rows, err := q.Query(fmt.Sprintf(`
SELECT source_id, target_id, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM links
WHERE %v
ORDER BY id
`, where))
assert.Nil(t, err)

for rows.Next() {
var row linkRow
var sourceId int64
var targetId *int64
err = rows.Scan(&sourceId, &targetId, &row.Title, &row.Href, &row.Type, &row.IsExternal, &row.Rels, &row.Snippet, &row.SnippetStart, &row.SnippetEnd)
assert.Nil(t, err)
row.SourceId = core.NoteID(sourceId)
if targetId != nil {
row.TargetId = idPointer(*targetId)
}
links = append(links, row)
}
rows.Close()
assert.Nil(t, rows.Err())

return links
}
Loading