diff --git a/cli/COMMANDS.md b/cli/COMMANDS.md
index ec26b444..98ef03a7 100644
--- a/cli/COMMANDS.md
+++ b/cli/COMMANDS.md
@@ -38,7 +38,7 @@ dnote view
dnote view golang
# See details of a note
-dnote view golang 12
+dnote view 12
```
## dnote edit
@@ -48,11 +48,11 @@ _alias: e_
Edit a note.
```bash
-# Launch a text editor to edit a note with the given index.
-dnote edit linux 1
+# Launch a text editor to edit a note with the given id.
+dnote edit 12
-# Edit a note with the given index in the specified book with a content.
-dnote edit linux 1 -c "New Content"
+# Edit a note with the given id in the specified book with a content.
+dnote edit 12 -c "New Content"
```
## dnote remove
@@ -62,8 +62,8 @@ _alias: d_
Remove either a note or a book.
```bash
-# Remove the note with `index` in the specified book.
-dnote remove JS 1
+# Remove the note with an id in the specified book.
+dnote remove 1
# Remove the book with the `book name`.
dnote remove -b JS
diff --git a/cli/cmd/add/add.go b/cli/cmd/add/add.go
index b9e1f1c2..47990748 100644
--- a/cli/cmd/add/add.go
+++ b/cli/cmd/add/add.go
@@ -76,14 +76,28 @@ func isReservedName(name string) bool {
return false
}
+// ErrBookNameReserved is an error incidating that the specified book name is reserved
+var ErrBookNameReserved = errors.New("The book name is reserved")
+
+// ErrNumericBookName is an error for book names that only contain numbers
+var ErrNumericBookName = errors.New("The book name cannot contain only numbers")
+
+func validateBookName(name string) error {
+ if isReservedName(name) {
+ return ErrBookNameReserved
+ }
+
+ if utils.IsNumber(name) {
+ return ErrNumericBookName
+ }
+
+ return nil
+}
+
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
bookName := args[0]
- if isReservedName(bookName) {
- return errors.Errorf("book name '%s' is reserved", bookName)
- }
-
if content == "" {
fpath := core.GetDnoteTmpContentPath(ctx)
err := core.GetEditorInput(ctx, fpath, &content)
@@ -97,13 +111,19 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
ts := time.Now().UnixNano()
- err := writeNote(ctx, bookName, content, ts)
+ noteRowID, err := writeNote(ctx, bookName, content, ts)
if err != nil {
return errors.Wrap(err, "Failed to write note")
}
log.Successf("added to %s\n", bookName)
- log.PrintContent(content)
+
+ info, err := core.GetNoteInfo(ctx, noteRowID)
+ if err != nil {
+ return err
+ }
+
+ core.PrintNoteInfo(info)
if err := core.CheckUpdate(ctx); err != nil {
log.Error(errors.Wrap(err, "automatically checking updates").Error())
@@ -113,10 +133,10 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
-func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) error {
+func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) (string, error) {
tx, err := ctx.DB.Begin()
if err != nil {
- return errors.Wrap(err, "beginning a transaction")
+ return "", errors.Wrap(err, "beginning a transaction")
}
var bookUUID string
@@ -128,10 +148,10 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
err = b.Insert(tx)
if err != nil {
tx.Rollback()
- return errors.Wrap(err, "creating the book")
+ return "", errors.Wrap(err, "creating the book")
}
} else if err != nil {
- return errors.Wrap(err, "finding the book")
+ return "", errors.Wrap(err, "finding the book")
}
noteUUID := utils.GenerateUUID()
@@ -140,10 +160,19 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
err = n.Insert(tx)
if err != nil {
tx.Rollback()
- return errors.Wrap(err, "creating the note")
+ return "", errors.Wrap(err, "creating the note")
+ }
+
+ var noteRowID string
+ err = tx.QueryRow(`SELECT notes.rowid
+ FROM notes
+ WHERE notes.uuid = ?`, noteUUID).
+ Scan(¬eRowID)
+ if err != nil {
+ return noteRowID, errors.Wrap(err, "getting the note rowid")
}
tx.Commit()
- return nil
+ return noteRowID, nil
}
diff --git a/cli/cmd/add/add_test.go b/cli/cmd/add/add_test.go
new file mode 100644
index 00000000..0bc9c185
--- /dev/null
+++ b/cli/cmd/add/add_test.go
@@ -0,0 +1,85 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote CLI.
+ *
+ * Dnote CLI is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote CLI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Dnote CLI. If not, see .
+ */
+
+package add
+
+import (
+ "testing"
+
+ "github.com/dnote/dnote/cli/testutils"
+)
+
+func TestValidateBookName(t *testing.T) {
+ testCases := []struct {
+ input string
+ expected error
+ }{
+ {
+ input: "javascript",
+ expected: nil,
+ },
+ {
+ input: "node.js",
+ expected: nil,
+ },
+ {
+ input: "foo bar",
+ expected: nil,
+ },
+ {
+ input: "123",
+ expected: ErrNumericBookName,
+ },
+ {
+ input: "+123",
+ expected: nil,
+ },
+ {
+ input: "-123",
+ expected: nil,
+ },
+ {
+ input: "+javascript",
+ expected: nil,
+ },
+ {
+ input: "0",
+ expected: ErrNumericBookName,
+ },
+ {
+ input: "0333",
+ expected: ErrNumericBookName,
+ },
+
+ // reserved book names
+ {
+ input: "trash",
+ expected: ErrBookNameReserved,
+ },
+ {
+ input: "conflicts",
+ expected: ErrBookNameReserved,
+ },
+ }
+
+ for _, tc := range testCases {
+ actual := validateBookName(tc.input)
+
+ testutils.AssertEqual(t, actual, tc.expected, "result does not match")
+ }
+}
diff --git a/cli/cmd/cat/cat.go b/cli/cmd/cat/cat.go
index 60ccbd6e..253fa87f 100644
--- a/cli/cmd/cat/cat.go
+++ b/cli/cmd/cat/cat.go
@@ -19,10 +19,6 @@
package cat
import (
- "database/sql"
- "fmt"
- "time"
-
"github.com/dnote/dnote/cli/core"
"github.com/dnote/dnote/cli/infra"
"github.com/dnote/dnote/cli/log"
@@ -63,50 +59,25 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
return cmd
}
-type noteInfo struct {
- BookLabel string
- UUID string
- Content string
- AddedOn int64
- EditedOn int64
-}
-
// NewRun returns a new run function
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
- db := ctx.DB
- bookLabel := args[0]
- noteRowID := args[1]
+ var noteRowID string
- var bookUUID string
- err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
- if err == sql.ErrNoRows {
- return errors.Errorf("book '%s' not found", bookLabel)
- } else if err != nil {
- return errors.Wrap(err, "querying the book")
+ if len(args) == 2 {
+ log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
+
+ noteRowID = args[1]
+ } else {
+ noteRowID = args[0]
}
- var info noteInfo
- err = db.QueryRow(`SELECT books.label, notes.uuid, notes.body, notes.added_on, notes.edited_on
- FROM notes
- INNER JOIN books ON books.uuid = notes.book_uuid
- WHERE notes.rowid = ? AND books.uuid = ?`, noteRowID, bookUUID).
- Scan(&info.BookLabel, &info.UUID, &info.Content, &info.AddedOn, &info.EditedOn)
- if err == sql.ErrNoRows {
- return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
- } else if err != nil {
- return errors.Wrap(err, "querying the note")
+ info, err := core.GetNoteInfo(ctx, noteRowID)
+ if err != nil {
+ return err
}
- log.Infof("book name: %s\n", info.BookLabel)
- log.Infof("note uuid: %s\n", info.UUID)
- log.Infof("created at: %s\n", time.Unix(0, info.AddedOn).Format("Jan 2, 2006 3:04pm (MST)"))
- if info.EditedOn != 0 {
- log.Infof("updated at: %s\n", time.Unix(0, info.EditedOn).Format("Jan 2, 2006 3:04pm (MST)"))
- }
- fmt.Printf("\n------------------------content------------------------\n")
- fmt.Printf("%s", info.Content)
- fmt.Printf("\n-------------------------------------------------------\n")
+ core.PrintNoteInfo(info)
return nil
}
diff --git a/cli/cmd/edit/edit.go b/cli/cmd/edit/edit.go
index 23665005..575f3f50 100644
--- a/cli/cmd/edit/edit.go
+++ b/cli/cmd/edit/edit.go
@@ -34,11 +34,11 @@ import (
var newContent string
var example = `
- * Edit the note by index in a book
- dnote edit js 3
+ * Edit the note by its id
+ dnote edit 3
* Skip the prompt by providing new content directly
- dnote edit js 3 -c "new content"`
+ dnote edit 3 -c "new content"`
// NewCmd returns a new edit command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
@@ -58,7 +58,7 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
}
func preRun(cmd *cobra.Command, args []string) error {
- if len(args) != 2 {
+ if len(args) > 2 {
return errors.New("Incorrect number of argument")
}
@@ -68,18 +68,21 @@ func preRun(cmd *cobra.Command, args []string) error {
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
db := ctx.DB
- bookLabel := args[0]
- noteRowID := args[1]
- bookUUID, err := core.GetBookUUID(ctx, bookLabel)
- if err != nil {
- return errors.Wrap(err, "finding book uuid")
+ var noteRowID string
+
+ if len(args) == 2 {
+ log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
+
+ noteRowID = args[1]
+ } else {
+ noteRowID = args[0]
}
var noteUUID, oldContent string
- err = db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND book_uuid = ?", noteRowID, bookUUID).Scan(¬eUUID, &oldContent)
+ err := db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND deleted = false", noteRowID).Scan(¬eUUID, &oldContent)
if err == sql.ErrNoRows {
- return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
+ return errors.Errorf("note %s not found", noteRowID)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
@@ -112,7 +115,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
_, err = tx.Exec(`UPDATE notes
SET body = ?, edited_on = ?, dirty = ?
- WHERE rowid = ? AND book_uuid = ?`, newContent, ts, true, noteRowID, bookUUID)
+ WHERE rowid = ?`, newContent, ts, true, noteRowID)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "updating the note")
diff --git a/cli/cmd/remove/remove.go b/cli/cmd/remove/remove.go
index 781279a4..7e6f9d0b 100644
--- a/cli/cmd/remove/remove.go
+++ b/cli/cmd/remove/remove.go
@@ -19,7 +19,6 @@
package remove
import (
- "database/sql"
"fmt"
"github.com/dnote/dnote/cli/core"
@@ -33,8 +32,8 @@ import (
var targetBookName string
var example = `
- * Delete a note by its index from a book
- dnote delete js 2
+ * Delete a note by its id
+ dnote delete 2
* Delete a book
dnote delete -b js`
@@ -65,14 +64,18 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return nil
}
- if len(args) < 2 {
+ var noteRowID string
+ if len(args) == 2 {
+ log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
+
+ noteRowID = args[1]
+ } else if len(args) == 1 {
+ noteRowID = args[0]
+ } else {
return errors.New("Missing argument")
}
- targetBook := args[0]
- noteRowID := args[1]
-
- if err := removeNote(ctx, noteRowID, targetBook); err != nil {
+ if err := removeNote(ctx, noteRowID); err != nil {
return errors.Wrap(err, "removing the note")
}
@@ -80,24 +83,15 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
-func removeNote(ctx infra.DnoteCtx, noteRowID, bookLabel string) error {
+func removeNote(ctx infra.DnoteCtx, noteRowID string) error {
db := ctx.DB
- bookUUID, err := core.GetBookUUID(ctx, bookLabel)
+ noteInfo, err := core.GetNoteInfo(ctx, noteRowID)
if err != nil {
- return errors.Wrap(err, "finding book uuid")
+ return err
}
- var noteUUID, noteContent string
- err = db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND book_uuid = ?", noteRowID, bookUUID).Scan(¬eUUID, ¬eContent)
- if err == sql.ErrNoRows {
- return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
- } else if err != nil {
- return errors.Wrap(err, "querying the book")
- }
-
- // todo: multiline
- log.Printf("body: \"%s\"\n", noteContent)
+ core.PrintNoteInfo(noteInfo)
ok, err := utils.AskConfirmation("remove this note?", false)
if err != nil {
@@ -113,12 +107,12 @@ func removeNote(ctx infra.DnoteCtx, noteRowID, bookLabel string) error {
return errors.Wrap(err, "beginning a transaction")
}
- if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, body = ? WHERE uuid = ? AND book_uuid = ?", true, true, "", noteUUID, bookUUID); err != nil {
+ if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, body = ? WHERE uuid = ?", true, true, "", noteInfo.UUID); err != nil {
return errors.Wrap(err, "removing the note")
}
tx.Commit()
- log.Successf("removed from %s\n", bookLabel)
+ log.Successf("removed from %s\n", noteInfo.BookLabel)
return nil
}
diff --git a/cli/cmd/view/view.go b/cli/cmd/view/view.go
index b8a55caa..6dae78a6 100644
--- a/cli/cmd/view/view.go
+++ b/cli/cmd/view/view.go
@@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/cli/cmd/cat"
"github.com/dnote/dnote/cli/cmd/ls"
+ "github.com/dnote/dnote/cli/utils"
)
var example = `
@@ -65,9 +66,16 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
var run core.RunEFunc
- if len(args) <= 1 {
+ if len(args) == 0 {
run = ls.NewRun(ctx)
+ } else if len(args) == 1 {
+ if utils.IsNumber(args[0]) {
+ run = cat.NewRun(ctx)
+ } else {
+ run = ls.NewRun(ctx)
+ }
} else if len(args) == 2 {
+ // DEPRECATED: passing book name to view command is deprecated
run = cat.NewRun(ctx)
} else {
return errors.New("Incorrect number of arguments")
diff --git a/cli/log/output.go b/cli/core/output.go
similarity index 57%
rename from cli/log/output.go
rename to cli/core/output.go
index 3bc0844b..092a0218 100644
--- a/cli/log/output.go
+++ b/cli/core/output.go
@@ -16,15 +16,26 @@
* along with Dnote CLI. If not, see .
*/
-package log
+package core
import (
"fmt"
+ "time"
+
+ "github.com/dnote/dnote/cli/log"
)
-// PrintContent prints the note content with an appropriate format.
-func PrintContent(content string) {
- fmt.Printf("\n-----------------------content-----------------------\n")
- fmt.Printf("%s", content)
- fmt.Printf("\n-----------------------------------------------------\n")
+// PrintNoteInfo prints a note information
+func PrintNoteInfo(info NoteInfo) {
+ log.Infof("book name: %s\n", info.BookLabel)
+ log.Infof("created at: %s\n", time.Unix(0, info.AddedOn).Format("Jan 2, 2006 3:04pm (MST)"))
+ if info.EditedOn != 0 {
+ log.Infof("updated at: %s\n", time.Unix(0, info.EditedOn).Format("Jan 2, 2006 3:04pm (MST)"))
+ }
+ log.Infof("note id: %d\n", info.RowID)
+ log.Infof("note uuid: %s\n", info.UUID)
+
+ fmt.Printf("\n------------------------content------------------------\n")
+ fmt.Printf("%s", info.Content)
+ fmt.Printf("\n-------------------------------------------------------\n")
}
diff --git a/cli/core/queries.go b/cli/core/queries.go
new file mode 100644
index 00000000..66d078cb
--- /dev/null
+++ b/cli/core/queries.go
@@ -0,0 +1,38 @@
+package core
+
+import (
+ "database/sql"
+
+ "github.com/dnote/dnote/cli/infra"
+ "github.com/pkg/errors"
+)
+
+// NoteInfo is a basic information about a note
+type NoteInfo struct {
+ RowID int
+ BookLabel string
+ UUID string
+ Content string
+ AddedOn int64
+ EditedOn int64
+}
+
+// GetNoteInfo returns a NoteInfo for the note with the given noteRowID
+func GetNoteInfo(ctx infra.DnoteCtx, noteRowID string) (NoteInfo, error) {
+ var ret NoteInfo
+
+ db := ctx.DB
+ err := db.QueryRow(`SELECT books.label, notes.uuid, notes.body, notes.added_on, notes.edited_on, notes.rowid
+ FROM notes
+ INNER JOIN books ON books.uuid = notes.book_uuid
+ WHERE notes.rowid = ? AND notes.deleted = false`, noteRowID).
+ Scan(&ret.BookLabel, &ret.UUID, &ret.Content, &ret.AddedOn, &ret.EditedOn, &ret.RowID)
+ if err == sql.ErrNoRows {
+ return ret, errors.Errorf("note %s not found", noteRowID)
+ } else if err != nil {
+ return ret, errors.Wrap(err, "querying the note")
+ }
+
+ return ret, nil
+
+}
diff --git a/cli/main_test.go b/cli/main_test.go
index f34a7a78..8d0aeb1d 100644
--- a/cli/main_test.go
+++ b/cli/main_test.go
@@ -52,7 +52,7 @@ func TestInit(t *testing.T) {
testutils.RunDnoteCmd(t, ctx, binaryName)
// Test
- if !utils.FileExists(fmt.Sprintf("%s", ctx.DnoteDir)) {
+ if !utils.FileExists(ctx.DnoteDir) {
t.Errorf("dnote directory was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ConfigFilename)) {
@@ -168,7 +168,7 @@ func TestEditNote_BodyFlag(t *testing.T) {
testutils.Setup4(t, ctx)
// Execute
- testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "js", "2", "-c", "foo bar")
+ testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "2", "-c", "foo bar")
// Test
db := ctx.DB
diff --git a/cli/migrate/fixtures/local-10-pre-schema.sql b/cli/migrate/fixtures/local-10-pre-schema.sql
new file mode 100644
index 00000000..a9679e45
--- /dev/null
+++ b/cli/migrate/fixtures/local-10-pre-schema.sql
@@ -0,0 +1,50 @@
+CREATE TABLE books
+ (
+ uuid text PRIMARY KEY,
+ label text NOT NULL
+ , dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
+CREATE TABLE system
+ (
+ key string NOT NULL,
+ value text NOT NULL
+ );
+CREATE UNIQUE INDEX idx_books_label ON books(label);
+CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
+CREATE TABLE IF NOT EXISTS "notes"
+ (
+ uuid text NOT NULL,
+ book_uuid text NOT NULL,
+ body text NOT NULL,
+ added_on integer NOT NULL,
+ edited_on integer DEFAULT 0,
+ public bool DEFAULT false,
+ dirty bool DEFAULT false,
+ usn int DEFAULT 0 NOT NULL,
+ deleted bool DEFAULT false
+ );
+CREATE VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'")
+/* note_fts(body) */;
+CREATE TABLE IF NOT EXISTS 'note_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
+CREATE TABLE IF NOT EXISTS 'note_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
+CREATE TABLE IF NOT EXISTS 'note_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
+CREATE TABLE IF NOT EXISTS 'note_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID;
+CREATE TRIGGER notes_after_insert AFTER INSERT ON notes BEGIN
+ INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
+ END;
+CREATE TRIGGER notes_after_delete AFTER DELETE ON notes BEGIN
+ INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
+ END;
+CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
+ INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
+ INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
+ END;
+CREATE TABLE actions
+ (
+ uuid text PRIMARY KEY,
+ schema integer NOT NULL,
+ type text NOT NULL,
+ data text NOT NULL,
+ timestamp integer NOT NULL
+ );
+CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
+CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
diff --git a/cli/migrate/migrate.go b/cli/migrate/migrate.go
index b604dedb..8179d66c 100644
--- a/cli/migrate/migrate.go
+++ b/cli/migrate/migrate.go
@@ -43,6 +43,7 @@ var LocalSequence = []migration{
lm7,
lm8,
lm9,
+ lm10,
}
// RemoteSequence is a list of remote migrations to be run
diff --git a/cli/migrate/migrate_test.go b/cli/migrate/migrate_test.go
index 3173d256..28b7686e 100644
--- a/cli/migrate/migrate_test.go
+++ b/cli/migrate/migrate_test.go
@@ -890,6 +890,77 @@ func TestLocalMigration9(t *testing.T) {
testutils.AssertEqual(t, resCount, 1, "noteFtsCount mismatch")
}
+func TestLocalMigration10(t *testing.T) {
+ // set up
+ ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-10-pre-schema.sql", false)
+ defer testutils.TeardownEnv(ctx)
+
+ db := ctx.DB
+
+ b1UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "123")
+ b2UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "123 javascript")
+ b3UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 3", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b3UUID, "foo")
+ b4UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 4", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b4UUID, "+123")
+ b5UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 5", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b5UUID, "0123")
+ b6UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 6", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b6UUID, "javascript 123")
+ b7UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 7", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b7UUID, "123 (1)")
+ b8UUID := utils.GenerateUUID()
+ testutils.MustExec(t, "inserting book 8", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b8UUID, "5")
+
+ // Execute
+ tx, err := db.Begin()
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "beginning a transaction"))
+ }
+
+ err = lm10.run(ctx, tx)
+ if err != nil {
+ tx.Rollback()
+ t.Fatal(errors.Wrap(err, "failed to run"))
+ }
+
+ tx.Commit()
+
+ // Test
+
+ // assert that note_fts was populated with correct values
+ var b1Label, b2Label, b3Label, b4Label, b5Label, b6Label, b7Label, b8Label string
+ var b1Dirty, b2Dirty, b3Dirty, b4Dirty, b5Dirty, b6Dirty, b7Dirty, b8Dirty bool
+
+ testutils.MustScan(t, "getting b1", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
+ testutils.MustScan(t, "getting b2", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b2UUID), &b2Label, &b2Dirty)
+ testutils.MustScan(t, "getting b3", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b3UUID), &b3Label, &b3Dirty)
+ testutils.MustScan(t, "getting b4", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b4UUID), &b4Label, &b4Dirty)
+ testutils.MustScan(t, "getting b5", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b5UUID), &b5Label, &b5Dirty)
+ testutils.MustScan(t, "getting b6", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b6UUID), &b6Label, &b6Dirty)
+ testutils.MustScan(t, "getting b7", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b7UUID), &b7Label, &b7Dirty)
+ testutils.MustScan(t, "getting b8", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b8UUID), &b8Label, &b8Dirty)
+
+ testutils.AssertEqual(t, b1Label, "123 (2)", "b1Label mismatch")
+ testutils.AssertEqual(t, b1Dirty, true, "b1Dirty mismatch")
+ testutils.AssertEqual(t, b2Label, "123 javascript", "b2Label mismatch")
+ testutils.AssertEqual(t, b2Dirty, false, "b2Dirty mismatch")
+ testutils.AssertEqual(t, b3Label, "foo", "b3Label mismatch")
+ testutils.AssertEqual(t, b3Dirty, false, "b3Dirty mismatch")
+ testutils.AssertEqual(t, b4Label, "+123", "b4Label mismatch")
+ testutils.AssertEqual(t, b4Dirty, false, "b4Dirty mismatch")
+ testutils.AssertEqual(t, b5Label, "0123 (1)", "b5Label mismatch")
+ testutils.AssertEqual(t, b5Dirty, true, "b5Dirty mismatch")
+ testutils.AssertEqual(t, b6Label, "javascript 123", "b6Label mismatch")
+ testutils.AssertEqual(t, b6Dirty, false, "b6Dirty mismatch")
+ testutils.AssertEqual(t, b7Label, "123 (1)", "b7Label mismatch")
+ testutils.AssertEqual(t, b7Dirty, false, "b7Dirty mismatch")
+ testutils.AssertEqual(t, b8Label, "5 (1)", "b8Label mismatch")
+ testutils.AssertEqual(t, b8Dirty, true, "b8Dirty mismatch")
+}
+
func TestRemoteMigration1(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/remote-1-pre-schema.sql", false)
diff --git a/cli/migrate/migrations.go b/cli/migrate/migrations.go
index e624e2a4..dd3ecb02 100644
--- a/cli/migrate/migrations.go
+++ b/cli/migrate/migrations.go
@@ -22,6 +22,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
+ "regexp"
"github.com/dnote/actions"
"github.com/dnote/dnote/cli/client"
@@ -403,6 +404,66 @@ var lm9 = migration{
},
}
+var lm10 = migration{
+ name: "rename-number-only-book",
+ run: func(ctx infra.DnoteCtx, tx *infra.DB) error {
+ migrateBook := func(label string) error {
+ var uuid string
+
+ err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", label).Scan(&uuid)
+ if err != nil {
+ return errors.Wrap(err, "finding uuid")
+ }
+
+ for i := 1; ; i++ {
+ candidate := fmt.Sprintf("%s (%d)", label, i)
+
+ var count int
+ err := tx.QueryRow("SELECT count(*) FROM books WHERE label = ?", candidate).Scan(&count)
+ if err != nil {
+ return errors.Wrap(err, "counting candidate")
+ }
+
+ if count == 0 {
+ _, err := tx.Exec("UPDATE books SET label = ?, dirty = ? WHERE uuid = ?", candidate, true, uuid)
+ if err != nil {
+ return errors.Wrapf(err, "updating book '%s'", label)
+ }
+
+ break
+ }
+ }
+
+ return nil
+ }
+
+ rows, err := tx.Query("SELECT label FROM books")
+ defer rows.Close()
+ if err != nil {
+ return errors.Wrap(err, "getting labels")
+ }
+
+ var regexNumber = regexp.MustCompile(`^\d+$`)
+
+ for rows.Next() {
+ var label string
+ err := rows.Scan(&label)
+ if err != nil {
+ return errors.Wrap(err, "scannign row")
+ }
+
+ if regexNumber.MatchString(label) {
+ err = migrateBook(label)
+ if err != nil {
+ return errors.Wrapf(err, "migrating book %s", label)
+ }
+ }
+ }
+
+ return nil
+ },
+}
+
var rm1 = migration{
name: "sync-book-uuids-from-server",
run: func(ctx infra.DnoteCtx, tx *infra.DB) error {
diff --git a/cli/testutils/fixtures/schema.sql b/cli/testutils/fixtures/schema.sql
index 6723608b..a9679e45 100644
--- a/cli/testutils/fixtures/schema.sql
+++ b/cli/testutils/fixtures/schema.sql
@@ -10,14 +10,6 @@ CREATE TABLE system
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
-CREATE TABLE actions
- (
- uuid text PRIMARY KEY,
- schema integer NOT NULL,
- type text NOT NULL,
- data text NOT NULL,
- timestamp integer NOT NULL
- );
CREATE TABLE IF NOT EXISTS "notes"
(
uuid text NOT NULL,
@@ -30,8 +22,7 @@ CREATE TABLE IF NOT EXISTS "notes"
usn int DEFAULT 0 NOT NULL,
deleted bool DEFAULT false
);
-CREATE VIRTUAL TABLE note_fts
- USING fts5(content=notes, body, tokenize = "unicode61")
+CREATE VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'")
/* note_fts(body) */;
CREATE TABLE IF NOT EXISTS 'note_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
CREATE TABLE IF NOT EXISTS 'note_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
@@ -47,5 +38,13 @@ CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
END;
+CREATE TABLE actions
+ (
+ uuid text PRIMARY KEY,
+ schema integer NOT NULL,
+ type text NOT NULL,
+ data text NOT NULL,
+ timestamp integer NOT NULL
+ );
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
diff --git a/cli/utils/utils.go b/cli/utils/utils.go
index f808075c..deb48d12 100644
--- a/cli/utils/utils.go
+++ b/cli/utils/utils.go
@@ -26,6 +26,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "regexp"
"strings"
"syscall"
@@ -248,3 +249,15 @@ func CopyFile(src, dest string) error {
return nil
}
+
+// regexNumber is a regex that matches a string that looks like an integer
+var regexNumber = regexp.MustCompile(`^\d+$`)
+
+// IsNumber checks if the given string is in the form of a number
+func IsNumber(s string) bool {
+ if s == "" {
+ return false
+ }
+
+ return regexNumber.MatchString(s)
+}