mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
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:
parent
0f13829209
commit
8d3e7cce6c
20 changed files with 766 additions and 117 deletions
32
Gopkg.lock
generated
32
Gopkg.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
[[constraint]]
|
||||
name = "github.com/dnote/actions"
|
||||
version = "0.1.0"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
|
|
|
|||
10
README.md
10
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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
main_test.go
12
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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
150
migrate/migrations.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package migrate
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue