| index f83ae5a..780fb43 100644 |
| --- a/apps/go/internal/todo/store.go |
| +++ b/apps/go/internal/todo/store.go |
| @@ -35,18 +35,19 @@ type Todo struct { |
| Completed bool |
| } |
| |
| -// migrations only ever grow. Each entry runs once, in order, and |
| -// PRAGMA user_version records how many have run. Never edit or remove |
| -// one that has shipped; append a new one. |
| +// migrations only ever grow. Each entry runs once, in order, recorded in |
| +// todo_migrations. Never edit or remove one that has shipped; append a |
| +// new one. Names are prefixed so they can't collide with tables an |
| +// earlier build may have left in the same file. |
| var migrations = []string{ |
| - `CREATE TABLE todos ( |
| + `CREATE TABLE todo_items ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| list BLOB NOT NULL, |
| title TEXT NOT NULL, |
| completed INTEGER NOT NULL DEFAULT 0, |
| created INTEGER NOT NULL DEFAULT (unixepoch()) |
| ); |
| - CREATE INDEX todos_by_list ON todos (list, id);`, |
| + CREATE INDEX todo_items_by_list ON todo_items (list, id);`, |
| } |
| |
| // Store is the todo database: one connection for writes, a small pool |
| @@ -92,8 +93,14 @@ func (s *Store) migrate(ctx context.Context) error { |
| return err |
| } |
| defer tx.Rollback() |
| + if _, err := tx.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS todo_migrations ( |
| + version INTEGER PRIMARY KEY, |
| + applied INTEGER NOT NULL DEFAULT (unixepoch()) |
| + )`); err != nil { |
| + return err |
| + } |
| var have int |
| - if err := tx.QueryRowContext(ctx, `PRAGMA user_version`).Scan(&have); err != nil { |
| + if err := tx.QueryRowContext(ctx, `SELECT COALESCE(MAX(version), 0) FROM todo_migrations`).Scan(&have); err != nil { |
| return err |
| } |
| if have > len(migrations) { |
| @@ -103,9 +110,9 @@ func (s *Store) migrate(ctx context.Context) error { |
| if _, err := tx.ExecContext(ctx, migrations[i]); err != nil { |
| return fmt.Errorf("todo: migration %d: %w", i+1, err) |
| } |
| - } |
| - if _, err := tx.ExecContext(ctx, fmt.Sprintf(`PRAGMA user_version = %d`, len(migrations))); err != nil { |
| - return err |
| + if _, err := tx.ExecContext(ctx, `INSERT INTO todo_migrations (version) VALUES (?)`, i+1); err != nil { |
| + return err |
| + } |
| } |
| return tx.Commit() |
| } |
| @@ -121,7 +128,7 @@ func (s *Store) Ping(ctx context.Context) error { return s.r.PingContext(ctx) } |
| // List returns every todo in the list, oldest first. |
| func (s *Store) List(ctx context.Context, k ListKey) ([]Todo, error) { |
| rows, err := s.r.QueryContext(ctx, |
| - `SELECT id, title, completed FROM todos WHERE list = ? ORDER BY id`, k[:]) |
| + `SELECT id, title, completed FROM todo_items WHERE list = ? ORDER BY id`, k[:]) |
| if err != nil { |
| return nil, err |
| } |
| @@ -148,14 +155,14 @@ func (s *Store) Add(ctx context.Context, k ListKey, title string) error { |
| if title = CleanTitle(title); title == "" { |
| return nil |
| } |
| - _, err := s.w.ExecContext(ctx, `INSERT INTO todos (list, title) VALUES (?, ?)`, k[:], title) |
| + _, err := s.w.ExecContext(ctx, `INSERT INTO todo_items (list, title) VALUES (?, ?)`, k[:], title) |
| return err |
| } |
| |
| // Toggle flips one todo between active and completed. |
| func (s *Store) Toggle(ctx context.Context, k ListKey, id int64) error { |
| return one(s.w.ExecContext(ctx, |
| - `UPDATE todos SET completed = 1 - completed WHERE id = ? AND list = ?`, id, k[:])) |
| + `UPDATE todo_items SET completed = 1 - completed WHERE id = ? AND list = ?`, id, k[:])) |
| } |
| |
| // Rename sets a todo's title. A title that is empty after cleaning |
| @@ -165,27 +172,27 @@ func (s *Store) Rename(ctx context.Context, k ListKey, id int64, title string) e |
| return s.Delete(ctx, k, id) |
| } |
| return one(s.w.ExecContext(ctx, |
| - `UPDATE todos SET title = ? WHERE id = ? AND list = ?`, title, id, k[:])) |
| + `UPDATE todo_items SET title = ? WHERE id = ? AND list = ?`, title, id, k[:])) |
| } |
| |
| // Delete removes one todo. |
| func (s *Store) Delete(ctx context.Context, k ListKey, id int64) error { |
| - return one(s.w.ExecContext(ctx, `DELETE FROM todos WHERE id = ? AND list = ?`, id, k[:])) |
| + return one(s.w.ExecContext(ctx, `DELETE FROM todo_items WHERE id = ? AND list = ?`, id, k[:])) |
| } |
| |
| // ToggleAll marks every todo completed, or, if every one already is, |
| // marks them all active. |
| func (s *Store) ToggleAll(ctx context.Context, k ListKey) error { |
| _, err := s.w.ExecContext(ctx, |
| - `UPDATE todos |
| - SET completed = EXISTS (SELECT 1 FROM todos WHERE list = ?1 AND completed = 0) |
| + `UPDATE todo_items |
| + SET completed = EXISTS (SELECT 1 FROM todo_items WHERE list = ?1 AND completed = 0) |
| WHERE list = ?1`, k[:]) |
| return err |
| } |
| |
| // ClearCompleted deletes every completed todo. |
| func (s *Store) ClearCompleted(ctx context.Context, k ListKey) error { |
| - _, err := s.w.ExecContext(ctx, `DELETE FROM todos WHERE list = ? AND completed = 1`, k[:]) |
| + _, err := s.w.ExecContext(ctx, `DELETE FROM todo_items WHERE list = ? AND completed = 1`, k[:]) |
| return err |
| } |
| |