Implement migration (#120)

* Make easier to copy paste to execute

* Make migration work

* Bump edit_note action schema to v3

* Use UnixNano for timstamps

* Fix schema
This commit is contained in:
Sung Won Cho 2018-10-07 20:37:02 +10:00 committed by GitHub
commit 8d3e7cce6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 766 additions and 117 deletions

32
Gopkg.lock generated
View file

@ -2,12 +2,12 @@
[[projects]]
digest = "1:97242fd82dcd5574a59c31685652a4de519934674a9fa21159604fdd5a005e7c"
branch = "master"
digest = "1:cd0089a5b5d872ac1b772087c7ee0ff2e71de50aa3a51826be64a63963a85287"
name = "github.com/dnote/actions"
packages = ["."]
pruneopts = ""
revision = "e646839669907194077733897c26ce2bb9856896"
version = "v0.1.0"
revision = "a1050f3f457804215c4ad50d92fbe1dd2e6b587c"
[[projects]]
digest = "1:e988ed0ca0d81f4d28772760c02ee95084961311291bdfefc1b04617c178b722"
@ -19,19 +19,19 @@
[[projects]]
branch = "master"
digest = "1:9f100ae40cada79ca20c068dc8510ad2d8decc49d84f27f9a45892cef3504557"
digest = "1:b4074c4585d29ae58161b728c7f64709b8a856fd724722e8159c7d5f9c6ab511"
name = "github.com/google/go-github"
packages = ["github"]
pruneopts = ""
revision = "d7732128a00e8e95e8fe896017da18ee20b2180d"
revision = "71b7a374a5fcfdca56ba35925f6ddba8b890fe60"
[[projects]]
branch = "master"
digest = "1:9abc49f39e3e23e262594bb4fb70abf74c0c99e94f99153f43b143805e850719"
digest = "1:cea4aa2038169ee558bf507d5ea02c94ca85bcca28a4c7bb99fd59b31e43a686"
name = "github.com/google/go-querystring"
packages = ["query"]
pruneopts = ""
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
revision = "44c6ddd0a2342c386950e880b658017258da92fc"
version = "v1.0.0"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
@ -50,12 +50,12 @@
version = "v0.0.9"
[[projects]]
digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c"
digest = "1:3140e04675a6a91d2a20ea9d10bdadf6072085502e6def6768361260aee4b967"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = ""
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
version = "v0.0.4"
[[projects]]
digest = "1:bc03901fc8f0965ccba8bc453eae21a9b04f95999eab664c7de6dc7290f4e8f4"
@ -74,12 +74,12 @@
version = "v0.8.0"
[[projects]]
digest = "1:6b55df4b0517a459af9d3879c99330af4367adcf45f3d0d37ded80a6272ae057"
digest = "1:7f569d906bdd20d906b606415b7d794f798f91a62fcfb6a4daa6d50690fb7a3f"
name = "github.com/satori/go.uuid"
packages = ["."]
pruneopts = ""
revision = "879c5887cd475cd7864858769793b2ceb0d44feb"
version = "v1.1.0"
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6"
@ -99,11 +99,11 @@
[[projects]]
branch = "master"
digest = "1:7a5f7a1206de6b90f67cb465e489eac3298e95afa7262813b542df4fab38952f"
digest = "1:149a432fabebb8221a80f77731b1cd63597197ded4f14af606ebe3a0959004ec"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = ""
revision = "4910a1d54f876d7b22162a85f4d066d3ee649450"
revision = "e4b3c5e9061176387e7cea65e4dc5853801f3fb7"
[[projects]]
branch = "v2"

View file

@ -43,7 +43,7 @@
[[constraint]]
name = "github.com/dnote/actions"
version = "0.1.0"
branch = "master"
[[constraint]]
name = "github.com/mattn/go-sqlite3"

View file

@ -11,6 +11,9 @@ On macOS, you can install using Homebrew:
```sh
brew tap dnote/dnote
brew install dnote
# to upgrade to the latest version
brew upgrade dnote
```
On Linux or macOS, you can use the installation script:
@ -35,13 +38,14 @@ Write technical notes without getting distracted from programming. The reasons a
- Add a note to a book named `linux`
```
$ dnote add linux -c "find - recursively walk the directory"
dnote add linux -c "find - recursively walk the directory"
```
- See the notes in a book
```
$ dnote view linux
dnote view linux
• on book linux
(0) find - recursively walk the directory
```
@ -53,7 +57,7 @@ Please refer to [commands](/COMMANDS.md).
## Links
- [Dnote](https://dnote.io)
- [Dnote Cloud](https://dnote.io/cloud)
- [Dnote Cloud](https://dnote.io/pricing)
- [Browser Extension](https://github.com/dnote/browser-extension)
## License

View file

@ -63,7 +63,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return errors.New("Empty content")
}
ts := time.Now().Unix()
ts := time.Now().UnixNano()
err := writeNote(ctx, bookName, content, ts)
if err != nil {
return errors.Wrap(err, "Failed to write note")

View file

@ -53,6 +53,7 @@ type noteInfo struct {
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
@ -81,9 +82,9 @@ func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
log.Infof("book name: %s\n", info.BookLabel)
log.Infof("note uuid: %s\n", info.UUID)
log.Infof("created at: %s\n", time.Unix(info.AddedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
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(info.EditedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
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)

View file

@ -84,7 +84,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return errors.New("Nothing changed")
}
ts := time.Now().Unix()
ts := time.Now().UnixNano()
newContent = core.SanitizeContent(newContent)
tx, err := db.Begin()

View file

@ -21,6 +21,7 @@ var example = `
* Delete a book
dnote delete -b js`
// NewCmd returns a new remove command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "remove",

View file

@ -41,7 +41,7 @@ func Prepare(ctx infra.DnoteCtx) error {
if err := migrate.Legacy(ctx); err != nil {
return errors.Wrap(err, "running legacy migration")
}
if err := migrate.Run(ctx); err != nil {
if err := migrate.Run(ctx, migrate.LocalSequence); err != nil {
return errors.Wrap(err, "running migration")
}

View file

@ -31,16 +31,15 @@ func LogActionAddNote(tx *sql.Tx, noteUUID, bookName, content string, timestamp
// LogActionRemoveNote logs an action for removing a book
func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
b, err := json.Marshal(actions.RemoveNoteDataV1{
b, err := json.Marshal(actions.RemoveNoteDataV2{
NoteUUID: noteUUID,
BookName: bookName,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().Unix()
if err := LogAction(tx, 1, actions.ActionRemoveNote, string(b), ts); err != nil {
ts := time.Now().UnixNano()
if err := LogAction(tx, 2, actions.ActionRemoveNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
@ -49,16 +48,17 @@ func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
// LogActionEditNote logs an action for editing a note
func LogActionEditNote(tx *sql.Tx, noteUUID, bookName, content string, ts int64) error {
b, err := json.Marshal(actions.EditNoteDataV2{
b, err := json.Marshal(actions.EditNoteDataV3{
NoteUUID: noteUUID,
FromBook: bookName,
Content: &content,
BookName: nil,
Public: nil,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
if err := LogAction(tx, 2, actions.ActionEditNote, string(b), ts); err != nil {
if err := LogAction(tx, 3, actions.ActionEditNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
@ -74,7 +74,7 @@ func LogActionAddBook(tx *sql.Tx, name string) error {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().Unix()
ts := time.Now().UnixNano()
if err := LogAction(tx, 1, actions.ActionAddBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
@ -89,7 +89,7 @@ func LogActionRemoveBook(tx *sql.Tx, name string) error {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().Unix()
ts := time.Now().UnixNano()
if err := LogAction(tx, 1, actions.ActionRemoveBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}

View file

@ -37,7 +37,7 @@ func TestLogActionEditNote(t *testing.T) {
Scan(&action.UUID, &action.Schema, &action.Type, &action.Timestamp, &action.Data); err != nil {
panic(errors.Wrap(err, "querying action"))
}
var actionData actions.EditNoteDataV2
var actionData actions.EditNoteDataV3
if err := json.Unmarshal(action.Data, &actionData); err != nil {
panic(errors.Wrap(err, "unmarshalling action data"))
}
@ -46,16 +46,11 @@ func TestLogActionEditNote(t *testing.T) {
t.Fatalf("action count mismatch. got %d", actionCount)
}
testutils.AssertNotEqual(t, action.UUID, "", "action uuid mismatch")
testutils.AssertEqual(t, action.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, action.Schema, 3, "action schema mismatch")
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
testutils.AssertEqual(t, *actionData.Content, "updated content", "action data content mismatch")
if actionData.ToBook != nil {
t.Errorf("action data to_book mismatch. Expected %+v. Got %+v", nil, actionData.ToBook)
}
if actionData.Public != nil {
t.Errorf("action data public mismatch. Expected %+v. Got %+v", nil, actionData.ToBook)
}
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
testutils.AssertEqual(t, actionData.Public, (*bool)(nil), "action data public mismatch")
}

View file

@ -26,6 +26,8 @@ func ReduceAll(ctx infra.DnoteCtx, tx *sql.Tx, actionSlice []actions.Action) err
// Reduce transitions the local dnote state by consuming the action returned
// from the server
func Reduce(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
log.Debug("reducing %s. uuid: %s, schema: %d, timestamp: %d\n", action.Type, action.UUID, action.Schema, action.Timestamp)
var err error
switch action.Type {
@ -63,12 +65,16 @@ func getBookUUIDWithTx(tx *sql.Tx, bookLabel string) (string, error) {
}
func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.AddNoteDataV1
if action.Schema != 2 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.AddNoteDataV2
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing add_note. action: %+v. data: %+v\n", action, data)
log.Debug("data: %+v\n", data)
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
if err != nil {
@ -76,8 +82,9 @@ func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error
}
var noteCount int
err = tx.QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).Scan(&noteCount)
if err != nil {
if err := tx.
QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).
Scan(&noteCount); err != nil {
return errors.Wrap(err, "counting note")
}
@ -91,7 +98,7 @@ func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error
_, err = tx.Exec(`INSERT INTO notes
(uuid, book_uuid, content, added_on, public)
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, false)
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, data.Public)
if err != nil {
return errors.Wrap(err, "inserting a note")
}
@ -100,12 +107,16 @@ func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error
}
func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.RemoveNoteDataV1
if action.Schema != 2 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.RemoveNoteDataV2
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing remove_note. action: %+v. data: %+v\n", action, data)
log.Debug("data: %+v\n", data)
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", data.NoteUUID)
if err != nil {
@ -115,7 +126,7 @@ func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) err
return nil
}
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID, bookUUID string, ts int64, data actions.EditNoteDataV2) (string, []interface{}, error) {
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID string, ts int64, data actions.EditNoteDataV3) (string, []interface{}, error) {
setTmpl := "edited_on = ?"
queryArgs := []interface{}{ts}
@ -127,37 +138,37 @@ func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID, bookUUID strin
setTmpl = fmt.Sprintf("%s, public = ?", setTmpl)
queryArgs = append(queryArgs, *data.Public)
}
if data.ToBook != nil {
bookUUID, err := getBookUUIDWithTx(tx, *data.ToBook)
if data.BookName != nil {
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
bookUUID, err := getBookUUIDWithTx(tx, *data.BookName)
if err != nil {
return "", []interface{}{}, errors.Wrap(err, "getting destination book uuid")
return setTmpl, queryArgs, errors.Wrap(err, "getting book uuid")
}
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
queryArgs = append(queryArgs, bookUUID)
}
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ? AND book_uuid = ?", setTmpl)
queryArgs = append(queryArgs, noteUUID, bookUUID)
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ?", setTmpl)
queryArgs = append(queryArgs, noteUUID)
return queryTmpl, queryArgs, nil
}
func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.EditNoteDataV2
if action.Schema != 3 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.EditNoteDataV3
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing edit_note v2. action: %+v. data: %+v\n", action, data)
log.Debug("data: %+v\n", data)
bookUUID, err := getBookUUIDWithTx(tx, data.FromBook)
if err != nil {
return errors.Wrap(err, "getting book uuid")
}
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, bookUUID, action.Timestamp, data)
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, action.Timestamp, data)
if err != nil {
return errors.Wrap(err, "building edit note query")
}
@ -170,13 +181,17 @@ func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error
}
func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 1 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.AddBookDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing add_book. action: %+v. data: %+v\n", action, data)
log.Debug("data: %+v\n", data)
var bookCount int
err = tx.QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).Scan(&bookCount)
@ -199,21 +214,33 @@ func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error
}
func handleRemoveBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 1 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.RemoveBookDataV1
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing remove_book. action: %+v. data: %+v\n", action, data)
log.Debug("data: %+v\n", data)
var bookUUID string
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", data.BookName).Scan(&bookUUID)
if err == sql.ErrNoRows {
var bookCount int
if err := tx.
QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).
Scan(&bookCount); err != nil {
return errors.Wrap(err, "counting note")
}
if bookCount == 0 {
// If book does not exist, another client added and removed the book, making the add_book action
// obsolete. noop.
return nil
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
if err != nil {
return errors.Wrap(err, "getting book uuid")
}
_, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID)

View file

@ -18,13 +18,15 @@ func TestReduceAddNote(t *testing.T) {
testutils.Setup1(t, ctx)
// Execute
b, err := json.Marshal(&actions.AddNoteDataV1{
b, err := json.Marshal(&actions.AddNoteDataV2{
Content: "new content",
BookName: "js",
NoteUUID: "06896551-8a06-4996-89cc-0d866308b0f6",
Public: false,
})
action := actions.Action{
Type: actions.ActionAddNote,
Schema: 2,
Data: b,
Timestamp: 1517629805,
}
@ -68,12 +70,12 @@ func TestReduceRemoveNote(t *testing.T) {
testutils.Setup2(t, ctx)
// Execute
b, err := json.Marshal(&actions.RemoveNoteDataV1{
BookName: "js",
b, err := json.Marshal(&actions.RemoveNoteDataV2{
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
})
action := actions.Action{
Type: actions.ActionRemoveNote,
Schema: 2,
Data: b,
Timestamp: 1517629805,
}
@ -122,7 +124,7 @@ func TestReduceEditNote(t *testing.T) {
expectedLinuxNoteCount int
}{
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "content": "updated content"}`,
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "updated content",
@ -133,7 +135,7 @@ func TestReduceEditNote(t *testing.T) {
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "public": true}`,
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "public": true}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "Date object implements mathematical comparisons",
@ -144,7 +146,7 @@ func TestReduceEditNote(t *testing.T) {
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "to_book": "linux", "content": "updated content"}`,
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "book_name": "linux", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "linux-book-uuid",
expectedNoteContent: "updated content",
@ -168,7 +170,7 @@ func TestReduceEditNote(t *testing.T) {
action := actions.Action{
Type: actions.ActionEditNote,
Data: json.RawMessage(tc.data),
Schema: 2,
Schema: 3,
Timestamp: 1517629805,
}
@ -225,6 +227,7 @@ func TestReduceAddBook(t *testing.T) {
b, err := json.Marshal(&actions.AddBookDataV1{BookName: "new_book"})
action := actions.Action{
Type: actions.ActionAddBook,
Schema: 1,
Data: b,
Timestamp: 1517629805,
}
@ -259,6 +262,7 @@ func TestReduceRemoveBook(t *testing.T) {
b, err := json.Marshal(&actions.RemoveBookDataV1{BookName: "linux"})
action := actions.Action{
Type: actions.ActionRemoveBook,
Schema: 1,
Data: b,
Timestamp: 1517629805,
}

View file

@ -17,6 +17,9 @@ import (
var (
// DnoteDirName is the name of the directory containing dnote files
DnoteDirName = ".dnote"
// SystemSchema is the key for schema in the system table
SystemSchema = "schema"
)
// DnoteCtx is a context holding the information of the current runtime

View file

@ -192,18 +192,15 @@ func TestEditNote_ContentFlag(t *testing.T) {
db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote),
&noteAction.Data, &noteAction.Type, &noteAction.Schema)
var actionData actions.EditNoteDataV2
var actionData actions.EditNoteDataV3
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
}
testutils.AssertEqual(t, noteAction.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Schema, 3, "action schema mismatch")
testutils.AssertEqual(t, *actionData.Content, "foo bar", "action data name mismatch")
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
if actionData.ToBook != nil {
t.Errorf("action data to_book mismatch. Expected %+v. Got %+v", nil, actionData.ToBook)
}
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
@ -262,10 +259,9 @@ func TestRemoveNote(t *testing.T) {
testutils.AssertEqual(t, b1.Name, "js", "b1 label mismatch")
testutils.AssertEqual(t, b2.Name, "linux", "b2 label mismatch")
testutils.AssertEqual(t, noteAction.Schema, 1, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionRemoveNote, "action type mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")

View file

@ -8,17 +8,19 @@ import (
"github.com/pkg/errors"
)
type migration struct {
name string
sql string
// LocalSequence is a list of local migrations to be run
var LocalSequence = []migration{
lm1,
lm2,
lm3,
}
var migrations = []migration{}
func initSchema(db *sql.DB) (int, error) {
func initSchema(ctx infra.DnoteCtx) (int, error) {
// schemaVersion is the index of the latest run migration in the sequence
schemaVersion := 0
_, err := db.Exec("INSERT INTO system (key, value) VALUES (? ,?)", "schema", schemaVersion)
db := ctx.DB
_, err := db.Exec("INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, schemaVersion)
if err != nil {
return schemaVersion, errors.Wrap(err, "inserting schema")
}
@ -26,12 +28,13 @@ func initSchema(db *sql.DB) (int, error) {
return schemaVersion, nil
}
func getSchema(db *sql.DB) (int, error) {
func getSchema(ctx infra.DnoteCtx) (int, error) {
var ret int
err := db.QueryRow("SELECT value FROM system where key = ?", "schema").Scan(&ret)
db := ctx.DB
err := db.QueryRow("SELECT value FROM system where key = ?", infra.SystemSchema).Scan(&ret)
if err == sql.ErrNoRows {
ret, err = initSchema(db)
ret, err = initSchema(ctx)
if err != nil {
return ret, errors.Wrap(err, "initializing schema")
@ -43,7 +46,7 @@ func getSchema(db *sql.DB) (int, error) {
return ret, nil
}
func execute(ctx infra.DnoteCtx, nextSchema int, m migration) error {
func execute(ctx infra.DnoteCtx, m migration) error {
log.Debug("running migration %s\n", m.name)
tx, err := ctx.DB.Begin()
@ -51,13 +54,20 @@ func execute(ctx infra.DnoteCtx, nextSchema int, m migration) error {
return errors.Wrap(err, "beginning a transaction")
}
_, err = tx.Exec(m.sql)
err = m.run(ctx, tx)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "running sql")
return errors.Wrapf(err, "running migration '%s'", m.name)
}
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", nextSchema, "schema")
var currentSchema int
err = tx.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema).Scan(&currentSchema)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "getting current schema")
}
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", currentSchema+1, infra.SystemSchema)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "incrementing schema")
@ -69,25 +79,18 @@ func execute(ctx infra.DnoteCtx, nextSchema int, m migration) error {
}
// Run performs unrun migrations
func Run(ctx infra.DnoteCtx) error {
db := ctx.DB
schema, err := getSchema(db)
func Run(ctx infra.DnoteCtx, migrations []migration) error {
schema, err := getSchema(ctx)
if err != nil {
return errors.Wrap(err, "getting the current schema")
}
log.Debug("current schema %d\n", schema)
if schema == len(migrations) {
return nil
}
log.Debug("current schema: %s %d of %d\n", infra.SystemSchema, schema, len(migrations))
toRun := migrations[schema:]
for idx, m := range toRun {
nextSchema := schema + idx + 1
if err := execute(ctx, nextSchema, m); err != nil {
for _, m := range toRun {
if err := execute(ctx, m); err != nil {
return errors.Wrapf(err, "running migration %s", m.name)
}
}

View file

@ -1,9 +1,454 @@
package migrate
import (
"database/sql"
"fmt"
"testing"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/testutils"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
)
func TestExecute(t *testing.T) {
func TestExecute_bump_schema(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 8)
m1 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
},
}
m2 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
},
}
// execute
err := execute(ctx, m1)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
err = execute(ctx, m2)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 10, "schema was not incremented properly")
}
func TestRun_nonfresh(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 2)
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
migration{
name: "v4",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v4 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v4")
return nil
},
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, fmt.Sprintf("getting schema for %s", infra.SystemSchema), db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 4, fmt.Sprintf("schema was not updated for %s", infra.SystemSchema))
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 2, "test run count mismatch")
var testRun1, testRun2 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v4"), &testRun2)
}
func TestRun_fresh(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 3, "test run count mismatch")
var testRun1, testRun2, testRun3 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v1"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v2"), &testRun2)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun3)
}
func TestRun_up_to_date(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 3)
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 0, "test run count mismatch")
}
func TestLocalMigration1(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
data := testutils.MustMarshalJSON(t, actions.AddBookDataV1{BookName: "js"})
a1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a1UUID, 1, "add_book", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV1{NoteUUID: "note-1-uuid", FromBook: "js", ToBook: "", Content: "note 1"})
a2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a2UUID, 1, "edit_note", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV1{NoteUUID: "note-2-uuid", FromBook: "js", ToBook: "", Content: "note 2"})
a3UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a3UUID, 1, "edit_note", string(data), 1537829463)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm1.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var actionCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.AssertEqual(t, actionCount, 3, "action count mismatch")
var a1, a2, a3 actions.Action
testutils.MustScan(t, "getting action 1", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a1UUID),
&a1.Schema, &a1.Type, &a1.Data, &a1.Timestamp)
testutils.MustScan(t, "getting action 2", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a2UUID),
&a2.Schema, &a2.Type, &a2.Data, &a2.Timestamp)
testutils.MustScan(t, "getting action 3", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a3UUID),
&a3.Schema, &a3.Type, &a3.Data, &a3.Timestamp)
var a1Data actions.AddBookDataV1
var a2Data, a3Data actions.EditNoteDataV3
testutils.MustUnmarshalJSON(t, a1.Data, &a1Data)
testutils.MustUnmarshalJSON(t, a2.Data, &a2Data)
testutils.MustUnmarshalJSON(t, a3.Data, &a3Data)
testutils.AssertEqual(t, a1.Schema, 1, "a1 schema mismatch")
testutils.AssertEqual(t, a1.Type, "add_book", "a1 type mismatch")
testutils.AssertEqual(t, a1.Timestamp, int64(1537829463), "a1 timestamp mismatch")
testutils.AssertEqual(t, a1Data.BookName, "js", "a1 data book_name mismatch")
testutils.AssertEqual(t, a2.Schema, 3, "a2 schema mismatch")
testutils.AssertEqual(t, a2.Type, "edit_note", "a2 type mismatch")
testutils.AssertEqual(t, a2.Timestamp, int64(1537829463), "a2 timestamp mismatch")
testutils.AssertEqual(t, a2Data.NoteUUID, "note-1-uuid", "a2 data note_uuid mismatch")
testutils.AssertEqual(t, a2Data.BookName, (*string)(nil), "a2 data book_name mismatch")
testutils.AssertEqual(t, *a2Data.Content, "note 1", "a2 data content mismatch")
testutils.AssertEqual(t, *a2Data.Public, false, "a2 data public mismatch")
testutils.AssertEqual(t, a3.Schema, 3, "a3 schema mismatch")
testutils.AssertEqual(t, a3.Type, "edit_note", "a3 type mismatch")
testutils.AssertEqual(t, a3.Timestamp, int64(1537829463), "a3 timestamp mismatch")
testutils.AssertEqual(t, a3Data.NoteUUID, "note-2-uuid", "a3 data note_uuid mismatch")
testutils.AssertEqual(t, a3Data.BookName, (*string)(nil), "a3 data book_name mismatch")
testutils.AssertEqual(t, *a3Data.Content, "note 2", "a3 data content mismatch")
testutils.AssertEqual(t, *a3Data.Public, false, "a3 data public mismatch")
}
func TestLocalMigration2(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
c1 := "note 1 - v1"
c2 := "note 1 - v2"
css := "css"
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "css")
data := testutils.MustMarshalJSON(t, actions.AddNoteDataV2{NoteUUID: "note-1-uuid", BookName: "js", Content: "note 1", Public: false})
a1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a1UUID, 2, "add_note", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV2{NoteUUID: "note-1-uuid", FromBook: "js", ToBook: nil, Content: &c1, Public: nil})
a2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a2UUID, 2, "edit_note", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV2{NoteUUID: "note-1-uuid", FromBook: "js", ToBook: &css, Content: &c2, Public: nil})
a3UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a3UUID, 2, "edit_note", string(data), 1537829463)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm2.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var actionCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.AssertEqual(t, actionCount, 3, "action count mismatch")
var a1, a2, a3 actions.Action
testutils.MustScan(t, "getting action 1", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a1UUID),
&a1.Schema, &a1.Type, &a1.Data, &a1.Timestamp)
testutils.MustScan(t, "getting action 2", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a2UUID),
&a2.Schema, &a2.Type, &a2.Data, &a2.Timestamp)
testutils.MustScan(t, "getting action 3", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a3UUID),
&a3.Schema, &a3.Type, &a3.Data, &a3.Timestamp)
var a1Data actions.AddNoteDataV2
var a2Data, a3Data actions.EditNoteDataV3
testutils.MustUnmarshalJSON(t, a1.Data, &a1Data)
testutils.MustUnmarshalJSON(t, a2.Data, &a2Data)
testutils.MustUnmarshalJSON(t, a3.Data, &a3Data)
testutils.AssertEqual(t, a1.Schema, 2, "a1 schema mismatch")
testutils.AssertEqual(t, a1.Type, "add_note", "a1 type mismatch")
testutils.AssertEqual(t, a1.Timestamp, int64(1537829463), "a1 timestamp mismatch")
testutils.AssertEqual(t, a1Data.NoteUUID, "note-1-uuid", "a1 data note_uuid mismatch")
testutils.AssertEqual(t, a1Data.BookName, "js", "a1 data book_name mismatch")
testutils.AssertEqual(t, a1Data.Public, false, "a1 data public mismatch")
testutils.AssertEqual(t, a2.Schema, 3, "a2 schema mismatch")
testutils.AssertEqual(t, a2.Type, "edit_note", "a2 type mismatch")
testutils.AssertEqual(t, a2.Timestamp, int64(1537829463), "a2 timestamp mismatch")
testutils.AssertEqual(t, a2Data.NoteUUID, "note-1-uuid", "a2 data note_uuid mismatch")
testutils.AssertEqual(t, a2Data.BookName, (*string)(nil), "a2 data book_name mismatch")
testutils.AssertEqual(t, *a2Data.Content, c1, "a2 data content mismatch")
testutils.AssertEqual(t, a2Data.Public, (*bool)(nil), "a2 data public mismatch")
testutils.AssertEqual(t, a3.Schema, 3, "a3 schema mismatch")
testutils.AssertEqual(t, a3.Type, "edit_note", "a3 type mismatch")
testutils.AssertEqual(t, a3.Timestamp, int64(1537829463), "a3 timestamp mismatch")
testutils.AssertEqual(t, a3Data.NoteUUID, "note-1-uuid", "a3 data note_uuid mismatch")
testutils.AssertEqual(t, *a3Data.BookName, "css", "a3 data book_name mismatch")
testutils.AssertEqual(t, *a3Data.Content, c2, "a3 data content mismatch")
testutils.AssertEqual(t, a3Data.Public, (*bool)(nil), "a3 data public mismatch")
}
func TestLocalMigration3(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
data := testutils.MustMarshalJSON(t, actions.AddNoteDataV2{NoteUUID: "note-1-uuid", BookName: "js", Content: "note 1", Public: false})
a1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a1UUID, 2, "add_note", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.RemoveNoteDataV1{NoteUUID: "note-1-uuid", BookName: "js"})
a2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a2UUID, 1, "remove_note", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.RemoveNoteDataV1{NoteUUID: "note-2-uuid", BookName: "js"})
a3UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a3UUID, 1, "remove_note", string(data), 1537829463)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm3.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var actionCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.AssertEqual(t, actionCount, 3, "action count mismatch")
var a1, a2, a3 actions.Action
testutils.MustScan(t, "getting action 1", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a1UUID),
&a1.Schema, &a1.Type, &a1.Data, &a1.Timestamp)
testutils.MustScan(t, "getting action 2", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a2UUID),
&a2.Schema, &a2.Type, &a2.Data, &a2.Timestamp)
testutils.MustScan(t, "getting action 3", db.QueryRow("SELECT schema, type, data, timestamp FROM actions WHERE uuid = ?", a3UUID),
&a3.Schema, &a3.Type, &a3.Data, &a3.Timestamp)
var a1Data actions.AddNoteDataV2
var a2Data, a3Data actions.RemoveNoteDataV2
testutils.MustUnmarshalJSON(t, a1.Data, &a1Data)
testutils.MustUnmarshalJSON(t, a2.Data, &a2Data)
testutils.MustUnmarshalJSON(t, a3.Data, &a3Data)
testutils.AssertEqual(t, a1.Schema, 2, "a1 schema mismatch")
testutils.AssertEqual(t, a1.Type, "add_note", "a1 type mismatch")
testutils.AssertEqual(t, a1.Timestamp, int64(1537829463), "a1 timestamp mismatch")
testutils.AssertEqual(t, a1Data.NoteUUID, "note-1-uuid", "a1 data note_uuid mismatch")
testutils.AssertEqual(t, a1Data.BookName, "js", "a1 data book_name mismatch")
testutils.AssertEqual(t, a1Data.Content, "note 1", "a1 data content mismatch")
testutils.AssertEqual(t, a1Data.Public, false, "a1 data public mismatch")
testutils.AssertEqual(t, a2.Schema, 2, "a2 schema mismatch")
testutils.AssertEqual(t, a2.Type, "remove_note", "a2 type mismatch")
testutils.AssertEqual(t, a2.Timestamp, int64(1537829463), "a2 timestamp mismatch")
testutils.AssertEqual(t, a2Data.NoteUUID, "note-1-uuid", "a2 data note_uuid mismatch")
testutils.AssertEqual(t, a3.Schema, 2, "a3 schema mismatch")
testutils.AssertEqual(t, a3.Type, "remove_note", "a3 type mismatch")
testutils.AssertEqual(t, a3.Timestamp, int64(1537829463), "a3 timestamp mismatch")
testutils.AssertEqual(t, a3Data.NoteUUID, "note-2-uuid", "a3 data note_uuid mismatch")
}

150
migrate/migrations.go Normal file
View file

@ -0,0 +1,150 @@
package migrate
import (
"database/sql"
"encoding/json"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/pkg/errors"
)
type migration struct {
name string
run func(ctx infra.DnoteCtx, tx *sql.Tx) error
}
var lm1 = migration{
name: "upgrade-edit-note-from-v1-to-v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
rows, err := tx.Query("SELECT uuid, data FROM actions WHERE type = ? AND schema = ?", "edit_note", 1)
if err != nil {
return errors.Wrap(err, "querying rows")
}
defer rows.Close()
f := false
for rows.Next() {
var uuid, dat string
err = rows.Scan(&uuid, &dat)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
var oldData actions.EditNoteDataV1
err = json.Unmarshal([]byte(dat), &oldData)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
newData := actions.EditNoteDataV3{
NoteUUID: oldData.NoteUUID,
Content: &oldData.Content,
// With edit_note v1, CLI did not support changing books or public
BookName: nil,
Public: &f,
}
b, err := json.Marshal(newData)
if err != nil {
return errors.Wrap(err, "marshalling new data")
}
_, err = tx.Exec("UPDATE actions SET data = ?, schema = ? WHERE uuid = ?", string(b), 3, uuid)
if err != nil {
return errors.Wrap(err, "updating a row")
}
}
return nil
},
}
var lm2 = migration{
name: "upgrade-edit-note-from-v2-to-v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
rows, err := tx.Query("SELECT uuid, data FROM actions WHERE type = ? AND schema = ?", "edit_note", 2)
if err != nil {
return errors.Wrap(err, "querying rows")
}
defer rows.Close()
for rows.Next() {
var uuid, dat string
err = rows.Scan(&uuid, &dat)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
var oldData actions.EditNoteDataV2
err = json.Unmarshal([]byte(dat), &oldData)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
newData := actions.EditNoteDataV3{
NoteUUID: oldData.NoteUUID,
BookName: oldData.ToBook,
Content: oldData.Content,
Public: oldData.Public,
}
b, err := json.Marshal(newData)
if err != nil {
return errors.Wrap(err, "marshalling new data")
}
_, err = tx.Exec("UPDATE actions SET data = ?, schema = ? WHERE uuid = ?", string(b), 3, uuid)
if err != nil {
return errors.Wrap(err, "updating a row")
}
}
return nil
},
}
var lm3 = migration{
name: "upgrade-remove-note-from-v1-to-v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
rows, err := tx.Query("SELECT uuid, data FROM actions WHERE type = ? AND schema = ?", "remove_note", 1)
if err != nil {
return errors.Wrap(err, "querying rows")
}
defer rows.Close()
for rows.Next() {
var uuid, dat string
err = rows.Scan(&uuid, &dat)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
var oldData actions.RemoveNoteDataV1
err = json.Unmarshal([]byte(dat), &oldData)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
newData := actions.RemoveNoteDataV2{
NoteUUID: oldData.NoteUUID,
}
b, err := json.Marshal(newData)
if err != nil {
return errors.Wrap(err, "marshalling new data")
}
_, err = tx.Exec("UPDATE actions SET data = ?, schema = ? WHERE uuid = ?", string(b), 2, uuid)
if err != nil {
return errors.Wrap(err, "updating a row")
}
}
return nil
},
}

View file

@ -1 +0,0 @@
package migrate

View file

@ -27,6 +27,7 @@ CREATE TABLE system
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE INDEX idx_books_uuid ON books(uuid);
CREATE INDEX idx_notes_id ON notes(id);
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -303,3 +303,23 @@ func UserConfirm(stdin io.WriteCloser) error {
return nil
}
// MustMarshalJSON marshalls the given interface into JSON.
// If there is any error, it fails the test.
func MustMarshalJSON(t *testing.T, v interface{}) []byte {
b, err := json.Marshal(v)
if err != nil {
t.Fatalf("%s: marshalling data", t.Name())
}
return b
}
// MustUnmarshalJSON marshalls the given interface into JSON.
// If there is any error, it fails the test.
func MustUnmarshalJSON(t *testing.T, data []byte, v interface{}) {
err := json.Unmarshal(data, v)
if err != nil {
t.Fatalf("%s: unmarshalling data", t.Name())
}
}