Simplify view and edit command (#184)

* Simplify the view command

* Simplify the edit command

* Migrate number-only book names

* Run migration

* Simplify remove

* Print note info when adding, editing, or removing

* Disallow users from editing or removing already removed notes
This commit is contained in:
Sung Won Cho 2019-05-18 16:52:12 +10:00 committed by GitHub
commit f526124243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 447 additions and 113 deletions

View file

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

View file

@ -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(&noteRowID)
if err != nil {
return noteRowID, errors.Wrap(err, "getting the note rowid")
}
tx.Commit()
return nil
return noteRowID, nil
}

85
cli/cmd/add/add_test.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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")
}
}

View file

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

View file

@ -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(&noteUUID, &oldContent)
err := db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND deleted = false", noteRowID).Scan(&noteUUID, &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")

View file

@ -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(&noteUUID, &noteContent)
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
}

View file

@ -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")

View file

@ -16,15 +16,26 @@
* along with Dnote CLI. If not, see <https://www.gnu.org/licenses/>.
*/
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")
}

38
cli/core/queries.go Normal file
View file

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

View file

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

View file

@ -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);

View file

@ -43,6 +43,7 @@ var LocalSequence = []migration{
lm7,
lm8,
lm9,
lm10,
}
// RemoteSequence is a list of remote migrations to be run

View file

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

View file

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

View file

@ -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);

View file

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