Save link types in the database

pull/104/head
Mickaël Menu 3 years ago
parent ea0024ea70
commit 397dce8749

@ -2,6 +2,7 @@ package sqlite
import (
"database/sql"
"fmt"
sqlite "github.com/mattn/go-sqlite3"
"github.com/mickael-menu/zk/internal/core"
@ -75,133 +76,142 @@ func (db *DB) migrate() error {
return err
}
needsReindexing := false
if version <= 0 {
err = tx.ExecStmts([]string{
// Notes
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
path TEXT NOT NULL,
sortable_path TEXT NOT NULL,
title TEXT DEFAULT('') NOT NULL,
lead TEXT DEFAULT('') NOT NULL,
body TEXT DEFAULT('') NOT NULL,
raw_content TEXT DEFAULT('') NOT NULL,
word_count INTEGER DEFAULT(0) NOT NULL,
checksum TEXT NOT NULL,
created DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
modified DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
UNIQUE(path)
)`,
`CREATE INDEX IF NOT EXISTS index_notes_checksum ON notes (checksum)`,
`CREATE INDEX IF NOT EXISTS index_notes_path ON notes (path)`,
// Links
`CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
source_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
target_id INTEGER REFERENCES notes(id)
ON DELETE SET NULL,
title TEXT DEFAULT('') NOT NULL,
href TEXT NOT NULL,
external INT DEFAULT(0) NOT NULL,
rels TEXT DEFAULT('') NOT NULL,
snippet TEXT DEFAULT('') NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS index_links_source_id_target_id ON links (source_id, target_id)`,
// FTS index
`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
path, title, body,
content = notes,
content_rowid = id,
tokenize = "porter unicode61 remove_diacritics 1 tokenchars '''&/'"
)`,
// Triggers to keep the FTS index up to date.
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`PRAGMA user_version = 1`,
})
if err != nil {
return err
}
migrations := []struct {
SQL []string
NeedsReindexing bool
}{
{ // 1
SQL: []string{
// Notes
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
path TEXT NOT NULL,
sortable_path TEXT NOT NULL,
title TEXT DEFAULT('') NOT NULL,
lead TEXT DEFAULT('') NOT NULL,
body TEXT DEFAULT('') NOT NULL,
raw_content TEXT DEFAULT('') NOT NULL,
word_count INTEGER DEFAULT(0) NOT NULL,
checksum TEXT NOT NULL,
created DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
modified DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
UNIQUE(path)
)`,
`CREATE INDEX IF NOT EXISTS index_notes_checksum ON notes (checksum)`,
`CREATE INDEX IF NOT EXISTS index_notes_path ON notes (path)`,
// Links
`CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
source_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
target_id INTEGER REFERENCES notes(id)
ON DELETE SET NULL,
title TEXT DEFAULT('') NOT NULL,
href TEXT NOT NULL,
external INT DEFAULT(0) NOT NULL,
rels TEXT DEFAULT('') NOT NULL,
snippet TEXT DEFAULT('') NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS index_links_source_id_target_id ON links (source_id, target_id)`,
// FTS index
`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
path, title, body,
content = notes,
content_rowid = id,
tokenize = "porter unicode61 remove_diacritics 1 tokenchars '''&/'"
)`,
// Triggers to keep the FTS index up to date.
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
},
},
{ // 2
SQL: []string{
// Collections
`CREATE TABLE IF NOT EXISTS collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
kind TEXT NO NULL,
name TEXT NOT NULL,
UNIQUE(kind, name)
)`,
`CREATE INDEX IF NOT EXISTS index_collections ON collections (kind, name)`,
// Note-Collection association
`CREATE TABLE IF NOT EXISTS notes_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
note_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
collection_id INTEGER NOT NULL REFERENCES collections(id)
ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS index_notes_collections ON notes_collections (note_id, collection_id)`,
// View of notes with their associated metadata (e.g. tags), for simpler queries.
`CREATE VIEW notes_with_metadata AS
SELECT n.*, GROUP_CONCAT(c.name, '` + "\x01" + `') AS tags
FROM notes n
LEFT JOIN notes_collections nc ON nc.note_id = n.id
LEFT JOIN collections c ON nc.collection_id = c.id AND c.kind = '` + string(core.CollectionKindTag) + `'
GROUP BY n.id`,
},
},
{ // 3
SQL: []string{
// Add a `metadata` column to `notes`
`ALTER TABLE notes ADD COLUMN metadata TEXT DEFAULT('{}') NOT NULL`,
// Add snippet's start and end offsets to `links`
`ALTER TABLE links ADD COLUMN snippet_start INTEGER DEFAULT(0) NOT NULL`,
`ALTER TABLE links ADD COLUMN snippet_end INTEGER DEFAULT(0) NOT NULL`,
},
NeedsReindexing: true,
},
{ // 4
SQL: []string{
// Metadata
`CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NO NULL
)`,
},
},
{ // 5
SQL: []string{
// Add a `type` column to `links`
`ALTER TABLE links ADD COLUMN type TEXT DEFAULT('') NOT NULL`,
},
NeedsReindexing: true,
},
}
if version <= 1 {
err = tx.ExecStmts([]string{
// Collections
`CREATE TABLE IF NOT EXISTS collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
kind TEXT NO NULL,
name TEXT NOT NULL,
UNIQUE(kind, name)
)`,
`CREATE INDEX IF NOT EXISTS index_collections ON collections (kind, name)`,
// Note-Collection association
`CREATE TABLE IF NOT EXISTS notes_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
note_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
collection_id INTEGER NOT NULL REFERENCES collections(id)
ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS index_notes_collections ON notes_collections (note_id, collection_id)`,
// View of notes with their associated metadata (e.g. tags), for simpler queries.
`CREATE VIEW notes_with_metadata AS
SELECT n.*, GROUP_CONCAT(c.name, '` + "\x01" + `') AS tags
FROM notes n
LEFT JOIN notes_collections nc ON nc.note_id = n.id
LEFT JOIN collections c ON nc.collection_id = c.id AND c.kind = '` + string(core.CollectionKindTag) + `'
GROUP BY n.id`,
`PRAGMA user_version = 2`,
})
if err != nil {
return err
}
}
if version <= 2 {
err = tx.ExecStmts([]string{
// Add a `metadata` column to `notes`
`ALTER TABLE notes ADD COLUMN metadata TEXT DEFAULT('{}') NOT NULL`,
// Add snippet's start and end offsets to `links`
`ALTER TABLE links ADD COLUMN snippet_start INTEGER DEFAULT(0) NOT NULL`,
`ALTER TABLE links ADD COLUMN snippet_end INTEGER DEFAULT(0) NOT NULL`,
needsReindexing := false
`PRAGMA user_version = 3`,
})
if err != nil {
return err
for i, migration := range migrations {
if version > i {
continue
}
needsReindexing = true
}
if version <= 3 {
err = tx.ExecStmts([]string{
// Metadata
`CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NO NULL
)`,
})
stmts := append(migration.SQL, fmt.Sprintf("PRAGMA user_version = %d", i+1))
err = tx.ExecStmts(stmts)
if err != nil {
return err
}
needsReindexing = needsReindexing || migration.NeedsReindexing
}
if needsReindexing {

@ -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, 3)
assert.Equal(t, version, 5)
_, err = tx.Exec(`
INSERT INTO notes (path, sortable_path, title, body, word_count, checksum)

@ -91,8 +91,8 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO {
// Add a new link.
addLinkStmt: tx.PrepareLazy(`
INSERT INTO links (source_id, target_id, title, href, external, rels, snippet, snippet_start, snippet_end)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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
@ -225,7 +225,7 @@ func (d *NoteDAO) addLinks(id core.NoteID, note core.Note) error {
return err
}
_, err = d.addLinkStmt.Exec(id, d.idToSql(targetId), link.Title, link.Href, link.IsExternal, joinLinkRels(link.Rels), link.Snippet, link.SnippetStart, link.SnippetEnd)
_, err = d.addLinkStmt.Exec(id, d.idToSql(targetId), link.Title, link.Href, link.Type, link.IsExternal, joinLinkRels(link.Rels), link.Snippet, link.SnippetStart, link.SnippetEnd)
if err != nil {
return err
}

@ -315,6 +315,7 @@ func TestNoteDAOUpdateWithLinks(t *testing.T) {
{
Title: "A new link",
Href: "index",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("rel"),
Snippet: "[[A new link]]",
@ -322,6 +323,7 @@ func TestNoteDAOUpdateWithLinks(t *testing.T) {
{
Title: "An external link",
Href: "https://domain.com",
Type: core.LinkTypeMarkdown,
IsExternal: true,
Snippet: "[[An external link]]",
},
@ -336,6 +338,7 @@ func TestNoteDAOUpdateWithLinks(t *testing.T) {
TargetId: idPointer(3),
Title: "A new link",
Href: "index",
Type: "wiki-link",
Rels: "\x01rel\x01",
Snippet: "[[A new link]]",
},
@ -344,6 +347,7 @@ func TestNoteDAOUpdateWithLinks(t *testing.T) {
TargetId: nil,
Title: "An external link",
Href: "https://domain.com",
Type: "markdown",
IsExternal: true,
Snippet: "[[An external link]]",
},
@ -1157,18 +1161,18 @@ func queryNoteRow(tx Transaction, where string) (noteRow, error) {
}
type linkRow struct {
SourceId core.NoteID
TargetId *core.NoteID
Href, Title, Rels, Snippet string
SnippetStart, SnippetEnd int
IsExternal bool
SourceId core.NoteID
TargetId *core.NoteID
Href, Type, Title, Rels, Snippet string
SnippetStart, SnippetEnd int
IsExternal bool
}
func queryLinkRows(t *testing.T, tx Transaction, where string) []linkRow {
links := make([]linkRow, 0)
rows, err := tx.Query(fmt.Sprintf(`
SELECT source_id, target_id, title, href, external, rels, snippet, snippet_start, snippet_end
SELECT source_id, target_id, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM links
WHERE %v
ORDER BY id
@ -1179,7 +1183,7 @@ func queryLinkRows(t *testing.T, tx Transaction, where string) []linkRow {
var row linkRow
var sourceId int64
var targetId *int64
err = rows.Scan(&sourceId, &targetId, &row.Title, &row.Href, &row.IsExternal, &row.Rels, &row.Snippet, &row.SnippetStart, &row.SnippetEnd)
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 {

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b7ba9b3ab5296c19e6a7a61ee4ca43116840ef956a1491c670bd7591abbd313
oid sha256:ac4ece96d790615233beec6e47eb094200eba7178d65f4803a8df6232b9719c4
size 86016

Loading…
Cancel
Save