diff --git a/Gopkg.lock b/Gopkg.lock index 9556456c..b9cf4e2c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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" diff --git a/Gopkg.toml b/Gopkg.toml index abd248c0..de629096 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -43,7 +43,7 @@ [[constraint]] name = "github.com/dnote/actions" - version = "0.1.0" + branch = "master" [[constraint]] name = "github.com/mattn/go-sqlite3" diff --git a/README.md b/README.md index 72a08b05..9388d138 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/add/add.go b/cmd/add/add.go index 25f0bc93..f15ff369 100644 --- a/cmd/add/add.go +++ b/cmd/add/add.go @@ -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") diff --git a/cmd/cat/cat.go b/cmd/cat/cat.go index 0d2b0cb5..dedfbb7c 100644 --- a/cmd/cat/cat.go +++ b/cmd/cat/cat.go @@ -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) diff --git a/cmd/edit/edit.go b/cmd/edit/edit.go index 6c2118b2..501613e5 100644 --- a/cmd/edit/edit.go +++ b/cmd/edit/edit.go @@ -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() diff --git a/cmd/remove/remove.go b/cmd/remove/remove.go index dd349e70..005a011a 100644 --- a/cmd/remove/remove.go +++ b/cmd/remove/remove.go @@ -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", diff --git a/cmd/root/root.go b/cmd/root/root.go index 49c50570..916f2d84 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -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") } diff --git a/core/action.go b/core/action.go index a3ca675f..3a16a644 100644 --- a/core/action.go +++ b/core/action.go @@ -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") } diff --git a/core/action_test.go b/core/action_test.go index 81e0ef2b..20511a80 100644 --- a/core/action_test.go +++ b/core/action_test.go @@ -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") } diff --git a/core/reducer.go b/core/reducer.go index 8bbffc4c..85ca6409 100644 --- a/core/reducer.go +++ b/core/reducer.go @@ -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(¬eCount) - if err != nil { + if err := tx. + QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID). + Scan(¬eCount); 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) diff --git a/core/reducer_test.go b/core/reducer_test.go index 5a574460..ca1b0ab6 100644 --- a/core/reducer_test.go +++ b/core/reducer_test.go @@ -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, } diff --git a/infra/main.go b/infra/main.go index cc67272e..4a5d77a7 100644 --- a/infra/main.go +++ b/infra/main.go @@ -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 diff --git a/main_test.go b/main_test.go index e4c883d6..866dbc53 100644 --- a/main_test.go +++ b/main_test.go @@ -192,18 +192,15 @@ func TestEditNote_ContentFlag(t *testing.T) { db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote), ¬eAction.Data, ¬eAction.Type, ¬eAction.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") diff --git a/migrate/migrate.go b/migrate/migrate.go index 8a8e1663..2288343b 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -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(¤tSchema) + 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) } } diff --git a/migrate/migrate_test.go b/migrate/migrate_test.go index 9a2804f3..46103288 100644 --- a/migrate/migrate_test.go +++ b/migrate/migrate_test.go @@ -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") +} diff --git a/migrate/migrations.go b/migrate/migrations.go new file mode 100644 index 00000000..da58d7ef --- /dev/null +++ b/migrate/migrations.go @@ -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 + }, +} diff --git a/migrate/sql.go b/migrate/sql.go deleted file mode 100644 index 278a35f0..00000000 --- a/migrate/sql.go +++ /dev/null @@ -1 +0,0 @@ -package migrate diff --git a/testutils/fixtures/schema.sql b/testutils/fixtures/schema.sql index 1ca2dd6a..88e700d0 100644 --- a/testutils/fixtures/schema.sql +++ b/testutils/fixtures/schema.sql @@ -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); diff --git a/testutils/main.go b/testutils/main.go index afff4c64..61c06c86 100644 --- a/testutils/main.go +++ b/testutils/main.go @@ -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()) + } +}