diff --git a/src/storage/migration.go b/src/storage/migration.go new file mode 100644 index 0000000..a12b173 --- /dev/null +++ b/src/storage/migration.go @@ -0,0 +1,212 @@ +package storage + +import ( + "database/sql" + "fmt" + "log" +) + +var migrations = []func(*sql.Tx)error{ + m00_initial, + m01_feed_states_and_errors, + m02_on_delete_actions, +} + +var maxVersion = int64(len(migrations)) + +func migrate(db *sql.DB, log *log.Logger) error { + var version int64 + db.QueryRow("pragma user_version").Scan(&version); + + if version >= maxVersion { + return nil + } + + log.Printf("db version is %d. migrating to %d", version, len(migrations)) + + for v := version; v < maxVersion; v++ { + migratefunc := migrations[v] + var tx *sql.Tx + var err error + + log.Printf("[migration:%d] starting", v) + if tx, err = db.Begin(); err != nil { + log.Printf("[migration:%d] failed to start transaction", v) + return err + } + if err = migratefunc(tx); err != nil { + log.Printf("[migration:%d] failed to migrate", v) + tx.Rollback() + return err + } + if _, err = tx.Exec(fmt.Sprintf("pragma user_version = %d", v + 1)); err != nil { + log.Printf("[migration:%d] failed to bump version", v) + tx.Rollback() + return err + } + if err = tx.Commit(); err != nil { + log.Printf("[migration:%d] failed to commit changes", v) + return err + } + log.Printf("[migration:%d] done", v) + } + return nil +} + +func m00_initial(tx *sql.Tx) error { + sql := ` + create table if not exists folders ( + id integer primary key autoincrement, + title text not null, + is_expanded boolean not null default false + ); + + create unique index if not exists idx_folder_title on folders(title); + + create table if not exists feeds ( + id integer primary key autoincrement, + folder_id references folders(id), + title text not null, + description text, + link text, + feed_link text not null, + icon blob + ); + + create index if not exists idx_feed_folder_id on feeds(folder_id); + create unique index if not exists idx_feed_feed_link on feeds(feed_link); + + create table if not exists items ( + id integer primary key autoincrement, + guid string not null, + feed_id references feeds(id), + title text, + link text, + description text, + content text, + author text, + date datetime, + date_updated datetime, + date_arrived datetime, + status integer, + image text, + search_rowid integer + ); + + create index if not exists idx_item_feed_id on items(feed_id); + create index if not exists idx_item_status on items(status); + create index if not exists idx_item_search_rowid on items(search_rowid); + create unique index if not exists idx_item_guid on items(feed_id, guid); + + create table if not exists settings ( + key string primary key, + val blob + ); + + create virtual table if not exists search using fts4(title, description, content); + + create trigger if not exists del_item_search after delete on items begin + delete from search where rowid = old.search_rowid; + end; + ` + _, err := tx.Exec(sql) + return err +} + +func m01_feed_states_and_errors(tx *sql.Tx) error { + sql := ` + create table if not exists http_states ( + feed_id references feeds(id) unique, + last_refreshed datetime not null, + + -- http header fields -- + last_modified string not null, + etag string not null + ); + + create table if not exists feed_errors ( + feed_id references feeds(id) unique, + error string + ); + ` + _, err := tx.Exec(sql) + return err +} + +func m02_on_delete_actions(tx *sql.Tx) error { + sql := ` + -- 01. disable foreignkey constraint + pragma foreign_keys=off; + + -- 02. create altered tables + create table if not exists new_feeds ( + id integer primary key autoincrement, + folder_id references folders(id) on delete set null, + title text not null, + description text, + link text, + feed_link text not null, + icon blob + ); + create table if not exists new_items ( + id integer primary key autoincrement, + guid string not null, + feed_id references feeds(id) on delete cascade, + title text, + link text, + description text, + content text, + author text, + date datetime, + date_updated datetime, + date_arrived datetime, + status integer, + image text, + search_rowid integer + ); + create table if not exists new_http_states ( + feed_id references feeds(id) on delete cascade unique, + last_refreshed datetime not null, + last_modified string not null, + etag string not null + ); + create table if not exists new_feed_errors ( + feed_id references feeds(id) on delete cascade unique, + error string + ); + + -- 03. transfer content into new tables + insert into new_feeds select * from feeds; + insert into new_items select * from items; + insert into new_http_states select * from http_states; + insert into new_feed_errors select * from feed_errors; + + -- 04. drop old tables + drop table feeds; + drop table items; + drop table http_states; + drop table feed_errors; + + -- 05. rename new tables + alter table new_feeds rename to feeds; + alter table new_items rename to items; + alter table new_http_states rename to http_states; + alter table new_feed_errors rename to feed_errors; + + -- 06. reconstruct indexes & triggers + create index if not exists idx_feed_folder_id on feeds(folder_id); + create unique index if not exists idx_feed_feed_link on feeds(feed_link); + create index if not exists idx_item_feed_id on items(feed_id); + create index if not exists idx_item_status on items(status); + create index if not exists idx_item_search_rowid on items(search_rowid); + create unique index if not exists idx_item_guid on items(feed_id, guid); + create trigger if not exists del_item_search after delete on items begin + delete from search where rowid = old.search_rowid; + end; + + pragma foreign_key_check; + pragma foreign_keys=on; + ` + _, err := tx.Exec(sql) + return err +} diff --git a/src/storage/storage.go b/src/storage/storage.go index 63675d3..948ea88 100644 --- a/src/storage/storage.go +++ b/src/storage/storage.go @@ -4,91 +4,14 @@ import ( "database/sql" _ "github.com/mattn/go-sqlite3" "log" - "os" ) -var initQuery string = ` -create table if not exists folders ( - id integer primary key autoincrement, - title text not null, - is_expanded boolean not null default false -); - -create unique index if not exists idx_folder_title on folders(title); - -create table if not exists feeds ( - id integer primary key autoincrement, - folder_id references folders(id) on delete set null, - title text not null, - description text, - link text, - feed_link text not null, - icon blob -); - -create index if not exists idx_feed_folder_id on feeds(folder_id); -create unique index if not exists idx_feed_feed_link on feeds(feed_link); - -create table if not exists items ( - id integer primary key autoincrement, - guid string not null, - feed_id references feeds(id) on delete cascade, - title text, - link text, - description text, - content text, - author text, - date datetime, - date_updated datetime, - date_arrived datetime, - status integer, - image text, - search_rowid integer -); - -create index if not exists idx_item_feed_id on items(feed_id); -create index if not exists idx_item_status on items(status); -create index if not exists idx_item_search_rowid on items(search_rowid); -create unique index if not exists idx_item_guid on items(feed_id, guid); - -create table if not exists settings ( - key string primary key, - val blob -); - -create virtual table if not exists search using fts4(title, description, content); - -create trigger if not exists del_item_search after delete on items begin - delete from search where rowid = old.search_rowid; -end; - -create table if not exists http_states ( - feed_id references feeds(id) on delete cascade unique, - last_refreshed datetime not null, - - -- http header fields -- - last_modified string not null, - etag string not null -); - -create table if not exists feed_errors ( - feed_id references feeds(id) on delete cascade unique, - error string -); -` - type Storage struct { db *sql.DB log *log.Logger } -func New(path string, logger *log.Logger) (*Storage, error) { - if _, err := os.Stat(path); err != nil { - if !os.IsNotExist(err) { - return nil, err - } - } - +func New(path string, log *log.Logger) (*Storage, error) { db, err := sql.Open("sqlite3", path) if err != nil { return nil, err @@ -96,9 +19,8 @@ func New(path string, logger *log.Logger) (*Storage, error) { db.SetMaxOpenConns(1) - // TODO: migration for 'on delete' actions - if _, err := db.Exec(initQuery); err != nil { + if err = migrate(db, log); err != nil { return nil, err } - return &Storage{db: db, log: logger}, nil + return &Storage{db: db, log: log}, nil }