diff --git a/TODO b/TODO index 0d71afa0..a75856d0 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ -* Merge users_articles_read and users_articles_fav into a single table * Create indexes for foreign key columns that are used in queries * Non-fatal API errors * TinyRSS API emulation diff --git a/content/sql/article.go b/content/sql/article.go index a4f2495d..42fa1de7 100644 --- a/content/sql/article.go +++ b/content/sql/article.go @@ -174,6 +174,14 @@ func updateArticle(a content.Article, tx *sqlx.Tx, db *db.DB, logger webfw.Logge } func (ua *UserArticle) Read(read bool) { + ua.updateState(read, ua.Data().Favorite) +} + +func (ua *UserArticle) Favorite(favorite bool) { + ua.updateState(ua.Data().Read, favorite) +} + +func (ua *UserArticle) updateState(read, favorite bool) { if ua.HasErr() { return } @@ -185,7 +193,7 @@ func (ua *UserArticle) Read(read bool) { } login := ua.User().Data().Login - ua.logger.Infof("Marking user '%s' article '%d' as read: %v\n", login, d.Id, read) + ua.logger.Infof("Updating user '%s' article '%d' state: read = %v, fav = %v\n", login, d.Id, read, favorite) tx, err := ua.db.Beginx() if err != nil { @@ -194,88 +202,40 @@ func (ua *UserArticle) Read(read bool) { } defer tx.Rollback() - stmt, err := tx.Preparex(ua.db.SQL("delete_user_article_read")) + stmt, err := tx.Preparex(ua.db.SQL("update_user_article_state")) if err != nil { - ua.Err(err) + ua.Err(fmt.Errorf("Error updating article %s state: %v", ua, err)) return } defer stmt.Close() - _, err = stmt.Exec(login, d.Id) + res, err := stmt.Exec(read, favorite, login, d.Id) if err != nil { ua.Err(err) return } - d.Read = read - - if read { - stmt, err = tx.Preparex(ua.db.SQL("create_user_article_read")) + if num, err := res.RowsAffected(); err != nil || num == 0 { + stmt, err := tx.Preparex(ua.db.SQL("create_user_article_state")) if err != nil { - ua.Err(err) + ua.Err(fmt.Errorf("Error creating article %s state: %v", ua, err)) return } defer stmt.Close() - _, err = stmt.Exec(login, d.Id) - ua.Err(err) - } - - tx.Commit() - - ua.Data(d) -} - -func (ua *UserArticle) Favorite(favorite bool) { - if ua.HasErr() { - return - } - - d := ua.Data() - if d.Id == 0 { - ua.Err(content.NewValidationError(errors.New("Invalid article id"))) - return - } - - login := ua.User().Data().Login - ua.logger.Infof("Marking user '%s' article '%d' as favorite: %v\n", login, d.Id, favorite) - - tx, err := ua.db.Beginx() - if err != nil { - ua.Err(err) - return - } - defer tx.Rollback() - - stmt, err := tx.Preparex(ua.db.SQL("delete_user_article_favorite")) - - if err != nil { - ua.Err(err) - return - } - defer stmt.Close() - - _, err = stmt.Exec(login, d.Id) - if err != nil { - ua.Err(err) - return - } - - d.Favorite = favorite - - if favorite { - stmt, err = tx.Preparex(ua.db.SQL("create_user_article_favorite")) + _, err = stmt.Exec(login, d.Id, read, favorite) if err != nil { ua.Err(err) return } - defer stmt.Close() - _, err = stmt.Exec(login, d.Id) - ua.Err(err) } - tx.Commit() - - ua.Data(d) + if err := tx.Commit(); err == nil { + d.Read = read + d.Favorite = favorite + ua.Data(d) + } else { + ua.Err(err) + } } diff --git a/content/sql/db/base/article.go b/content/sql/db/base/article.go index dc3eeae3..8b5a15aa 100644 --- a/content/sql/db/base/article.go +++ b/content/sql/db/base/article.go @@ -3,10 +3,8 @@ package base func init() { sql["create_feed_article"] = createFeedArticle sql["update_feed_article"] = updateFeedArticle - sql["create_user_article_read"] = createUserArticleRead - sql["delete_user_article_read"] = deleteUserArticleRead - sql["create_user_article_favorite"] = createUserArticleFavorite - sql["delete_user_article_favorite"] = deleteUserArticleFavorite + sql["create_user_article_state"] = createUserArticleState + sql["update_user_article_state"] = updateUserArticleState sql["get_article_scores"] = getArticleScores sql["create_article_scores"] = createArticleScores sql["update_article_scores"] = updateArticleScores @@ -31,23 +29,18 @@ UPDATE articles SET title = $1, description = $2, date = $3, guid = $4, link = $ WHERE feed_id = $6 AND (guid = $4 OR link = $5) ` - createUserArticleRead = ` -INSERT INTO users_articles_read(user_login, article_id) - SELECT $1, $2 EXCEPT - SELECT user_login, article_id - FROM users_articles_read WHERE user_login = $1 AND article_id = $2 + createUserArticleState = ` +INSERT INTO users_articles_states(user_login, article_id, read, favorite) + SELECT $1, $2, $3, $4 EXCEPT + SELECT user_login, article_id, CAST($3 AS BOOLEAN), CAST($4 AS BOOLEAN) + FROM users_articles_states WHERE user_login = $1 AND article_id = $2 ` - deleteUserArticleRead = ` -DELETE FROM users_articles_read WHERE user_login = $1 AND article_id = $2` - createUserArticleFavorite = ` -INSERT INTO users_articles_fav(user_login, article_id) - SELECT $1, $2 EXCEPT - SELECT user_login, article_id - FROM users_articles_fav WHERE user_login = $1 AND article_id = $2 -` - deleteUserArticleFavorite = ` -DELETE FROM users_articles_fav WHERE user_login = $1 AND article_id = $2 + + updateUserArticleState = ` +UPDATE users_articles_states SET read = $1, favorite = $2 + WHERE user_login = $3 AND article_id = $4 ` + getArticleScores = ` SELECT asco.score, asco.score1, asco.score2, asco.score3, asco.score4, asco.score5 FROM articles_scores asco diff --git a/content/sql/db/base/feed.go b/content/sql/db/base/feed.go index 9f59080e..5d344faa 100644 --- a/content/sql/db/base/feed.go +++ b/content/sql/db/base/feed.go @@ -9,8 +9,8 @@ func init() { sql["get_hubbub_subscription"] = getHubbubSubscription sql["get_feed_users"] = getFeedUsers sql["delete_user_feed"] = deleteUserFeed - sql["create_all_users_articles_read_by_feed_date"] = createAllUsersArticlesReadByFeedDate - sql["delete_all_users_articles_read_by_feed_date"] = deleteAllUsersArticlesReadByFeedDate + sql["create_missing_user_article_state_by_feed_date"] = createMissingUserArticleStateByFeedDate + sql["update_all_user_article_state_by_feed_date"] = updateAllUserArticleReadStateByFeedDate sql["create_user_feed_tag"] = createUserFeedTag sql["delete_user_feed_tags"] = deleteUserFeedTags sql["get_user_feed_tags"] = getUserFeedTags @@ -43,20 +43,27 @@ SELECT u.login, u.first_name, u.last_name, u.email, u.admin, u.active, FROM users u, users_feeds uf WHERE u.login = uf.user_login AND uf.feed_id = $1 ` - deleteUserFeed = `DELETE FROM users_feeds WHERE user_login = $1 AND feed_id = $2` - createAllUsersArticlesReadByFeedDate = ` -INSERT INTO users_articles_read - SELECT uf.user_login, a.id - FROM users_feeds uf INNER JOIN articles a - ON uf.feed_id = a.feed_id AND uf.user_login = $1 AND uf.feed_id = $2 - AND a.id IN (SELECT id FROM articles WHERE date IS NULL OR date < $3) -` + deleteUserFeed = `DELETE FROM users_feeds WHERE user_login = $1 AND feed_id = $2` - deleteAllUsersArticlesReadByFeedDate = ` -DELETE FROM users_articles_read WHERE user_login = $1 AND article_id IN ( - SELECT id FROM articles WHERE feed_id = $2 AND (date IS NULL OR date < $3) + createMissingUserArticleStateByFeedDate = ` +INSERT INTO users_articles_states (user_login, article_id) +SELECT uf.user_login, a.id +FROM users_feeds uf INNER JOIN articles a + ON uf.feed_id = a.feed_id AND uf.user_login = $1 AND uf.feed_id = $2 + AND a.id IN ( + SELECT id FROM articles where date IS NULL OR date < $3 + ) +EXCEPT SELECT uas.user_login, uas.article_id +FROM articles a INNER JOIN users_articles_states uas + ON a.id = uas.article_id +WHERE uas.user_login = $1 AND a.feed_id = $2 +` + updateAllUserArticleReadStateByFeedDate = ` +UPDATE users_articles_states SET read = $1 WHERE user_login = $2 AND article_id IN ( + SELECT id FROM articles WHERE feed_id = $3 AND (date IS NULL OR date < $4) ) ` + getUserFeedTags = `SELECT tag FROM users_feeds_tags WHERE user_login = $1 AND feed_id = $2` createUserFeedTag = ` INSERT INTO users_feeds_tags(user_login, feed_id, tag) @@ -73,8 +80,8 @@ FROM users_feeds uf INNER JOIN articles a ON uf.feed_id = a.feed_id AND uf.user_login = $1 AND uf.feed_id = $2 -LEFT OUTER JOIN users_articles_read ar - ON a.id = ar.article_id AND uf.user_login = ar.user_login -WHERE ar.article_id IS NULL +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login +WHERE uas.article_id IS NULL OR NOT uas.read ` ) diff --git a/content/sql/db/base/tag.go b/content/sql/db/base/tag.go index 6782e143..fbfd011c 100644 --- a/content/sql/db/base/tag.go +++ b/content/sql/db/base/tag.go @@ -2,8 +2,8 @@ package base func init() { sql["get_user_tag_feeds"] = getUserTagFeeds - sql["create_all_user_tag_articles_read_by_date"] = createAllUserTagArticlesByDate - sql["delete_all_user_tag_articles_read_by_date"] = deleteAllUserTagArticlesByDate + sql["create_missing_user_article_state_by_tag_date"] = createMissingUserArticleStateByTagDate + sql["update_all_user_article_state_by_tag_date"] = updateAllUserArticleReadStateByTagDate sql["get_tag_unread_count"] = getTagUnreadCount } @@ -15,24 +15,31 @@ WHERE f.id = uft.feed_id AND uft.user_login = $1 AND uft.tag = $2 ORDER BY LOWER(f.title) ` - createAllUserTagArticlesByDate = ` -INSERT INTO users_articles_read - SELECT uf.user_login, a.id - FROM users_feeds uf INNER JOIN users_feeds_tags uft - ON uft.feed_id = uf.feed_id AND uft.user_login = uf.user_login - AND uft.user_login = $1 AND uft.tag = $2 - INNER JOIN articles a - ON uf.feed_id = a.feed_id - AND a.id IN (SELECT id FROM articles WHERE date IS NULL OR date < $3) -` - - deleteAllUserTagArticlesByDate = ` -DELETE FROM users_articles_read WHERE user_login = $1 - AND article_id IN ( - SELECT feed_id FROM users_feeds_tags WHERE user_login = $1 AND tag = $2 - ) AND article_id IN ( - SELECT id FROM articles WHERE date IS NULL OR date < $3 + createMissingUserArticleStateByTagDate = ` +INSERT INTO users_articles_states (user_login, article_id) +SELECT uf.user_login, a.id +FROM users_feeds uf INNER JOIN users_feeds_tags uft + ON uft.feed_id = uf.feed_id AND uft.user_login = uf.user_login + AND uft.user_login = $1 AND uft.tag = $2 +INNER JOIN articles a + ON uf.feed_id = a.feed_id + AND a.id IN ( + SELECT id FROM articles where date IS NULL OR date < $3 ) +EXCEPT SELECT uas.user_login, uas.article_id +FROM articles a INNER JOIN users_feeds_tags uft + ON a.feed_id = uft.feed_id +INNER JOIN users_articles_states uas + ON a.id = uas.article_id +WHERE uas.user_login = $1 AND uft.tag = $2 +` + updateAllUserArticleReadStateByTagDate = ` +UPDATE users_articles_states SET read = $1 WHERE user_login = $2 AND article_id IN ( + SELECT a.id + FROM articles a INNER JOIN users_feeds_tags uft + ON a.feed_id = uft.feed_id + WHERE uft.tag = $3 AND (date IS NULL OR date < $4) +) ` getTagUnreadCount = ` SELECT count(a.id) @@ -43,8 +50,8 @@ INNER JOIN users_feeds_tags uft ON uft.feed_id = uf.feed_id AND uft.user_login = uf.user_login AND uft.tag = $2 -LEFT OUTER JOIN users_articles_read ar - ON a.id = ar.article_id AND uf.user_login = ar.user_login -WHERE ar.article_id IS NULL +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login +WHERE uas.article_id IS NULL OR NOT uas.read ` ) diff --git a/content/sql/db/base/user.go b/content/sql/db/base/user.go index af7d42eb..ce395d10 100644 --- a/content/sql/db/base/user.go +++ b/content/sql/db/base/user.go @@ -15,10 +15,10 @@ func init() { sql["get_all_unread_user_article_ids"] = getAllUnreadUserArticleIds sql["get_all_favorite_user_article_ids"] = getAllFavoriteUserArticleIds sql["get_user_article_count"] = getUserArticleCount - sql["create_all_user_articles_read_by_date"] = createAllUserArticlesReadByDate - sql["delete_all_user_articles_read_by_date"] = deleteAllUserArticlesReadByDate - sql["create_newer_user_articles_read_by_date"] = createNewerUserArticlesReadByDate - sql["delete_newer_user_articles_read_by_date"] = deleteNewerUserArticlesReadByDate + sql["create_missing_user_article_state_by_date"] = createMissingUserArticleStateByDate + sql["update_all_user_article_state_by_date"] = updateAllUserArticleReadStateByDate + sql["create_newer_missing_user_article_state_by_date"] = createNewerMissingUserArticleStateByDate + sql["update_all_newer_user_article_state_by_date"] = updateAllNewerUserArticleReadStateByDate sql["get_user_unread_count"] = getUserUnreadCount } @@ -51,8 +51,8 @@ ORDER BY LOWER(f.title) getUserTags = `SELECT DISTINCT tag FROM users_feeds_tags WHERE user_login = $1` getArticleColumns = ` SELECT a.feed_id, a.id, a.title, a.description, a.link, a.date, a.guid, -CASE WHEN ar.article_id IS NULL THEN 0 ELSE 1 END AS read, -CASE WHEN af.article_id IS NULL THEN 0 ELSE 1 END AS favorite, +CASE WHEN uas.article_id IS NULL OR NOT uas.read THEN 0 ELSE 1 END AS read, +CASE WHEN uas.article_id IS NULL OR NOT uas.favorite THEN 0 ELSE 1 END AS favorite, COALESCE(at.thumbnail, '') as thumbnail, COALESCE(at.link, '') as thumbnail_link ` @@ -63,10 +63,8 @@ FROM users_feeds uf INNER JOIN articles a ` getArticleJoins = ` -LEFT OUTER JOIN users_articles_read ar - ON a.id = ar.article_id AND uf.user_login = ar.user_login -LEFT OUTER JOIN users_articles_fav af - ON a.id = af.article_id AND uf.user_login = af.user_login +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login LEFT OUTER JOIN articles_thumbnails at ON a.id = at.article_id WHERE uf.user_login = $1 @@ -75,45 +73,55 @@ WHERE uf.user_login = $1 SELECT a.id FROM users_feeds uf INNER JOIN articles a ON uf.feed_id = a.feed_id AND uf.user_login = $1 -LEFT OUTER JOIN users_articles_read ar - ON a.id = ar.article_id AND uf.user_login = ar.user_login -WHERE ar.article_id IS NULL +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login +WHERE uas.article_id IS NULL OR NOT uas.read ` getAllFavoriteUserArticleIds = ` SELECT a.id FROM users_feeds uf INNER JOIN articles a ON uf.feed_id = a.feed_id AND uf.user_login = $1 -LEFT OUTER JOIN users_articles_fav af - ON a.id = af.article_id AND uf.user_login = af.user_login -WHERE af.article_id IS NOT NULL +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login +WHERE uas.favorite ` getUserArticleCount = ` SELECT count(a.id) FROM users_feeds uf INNER JOIN articles a ON uf.feed_id = a.feed_id AND uf.user_login = $1 ` - createAllUserArticlesReadByDate = ` -INSERT INTO users_articles_read - SELECT uf.user_login, a.id - FROM users_feeds uf INNER JOIN articles a - ON uf.feed_id = a.feed_id AND uf.user_login = $1 - AND a.id IN (SELECT id FROM articles WHERE date IS NULL OR date < $2) + createMissingUserArticleStateByDate = ` +INSERT INTO users_articles_states (user_login, article_id) +SELECT uf.user_login, a.id +FROM users_feeds uf INNER JOIN articles a + ON uf.feed_id = a.feed_id AND uf.user_login = $1 + AND a.id IN ( + SELECT id FROM articles where date IS NULL OR date < $2 + ) +EXCEPT SELECT uas.user_login, uas.article_id +FROM users_articles_states uas +WHERE uas.user_login = $1 ` - deleteAllUserArticlesReadByDate = ` -DELETE FROM users_articles_read WHERE user_login = $1 AND article_id IN ( - SELECT id FROM articles WHERE date IS NULL OR date < $2 + updateAllUserArticleReadStateByDate = ` +UPDATE users_articles_states SET read = $1 WHERE user_login = $2 AND article_id IN ( + SELECT id FROM articles WHERE date IS NULL OR date < $3 ) ` - createNewerUserArticlesReadByDate = ` -INSERT INTO users_articles_read - SELECT uf.user_login, a.id - FROM users_feeds uf INNER JOIN articles a - ON uf.feed_id = a.feed_id AND uf.user_login = $1 - AND a.id IN (SELECT id FROM articles WHERE date > $2) + createNewerMissingUserArticleStateByDate = ` +INSERT INTO users_articles_states (user_login, article_id) +SELECT uf.user_login, a.id +FROM users_feeds uf INNER JOIN articles a + ON uf.feed_id = a.feed_id AND uf.user_login = $1 + AND a.id IN ( + SELECT id FROM articles where date > $2 + ) +EXCEPT SELECT uas.user_login, uas.article_id +FROM users_articles_states uas +WHERE uas.user_login = $1 ` - deleteNewerUserArticlesReadByDate = ` -DELETE FROM users_articles_read WHERE user_login = $1 AND article_id IN ( - SELECT id FROM articles WHERE date > $2 + updateAllNewerUserArticleReadStateByDate = ` +UPDATE users_articles_states SET read = $1 WHERE user_login = $2 AND article_id IN ( + SELECT id FROM articles WHERE date > $3 ) ` getUserUnreadCount = ` @@ -121,8 +129,8 @@ SELECT count(a.id) FROM users_feeds uf INNER JOIN articles a ON uf.feed_id = a.feed_id AND uf.user_login = $1 -LEFT OUTER JOIN users_articles_read ar - ON a.id = ar.article_id AND uf.user_login = ar.user_login -WHERE ar.article_id IS NULL +LEFT OUTER JOIN users_articles_states uas + ON a.id = uas.article_id AND uf.user_login = uas.user_login +WHERE uas.article_id IS NULL OR NOT uas.read ` ) diff --git a/content/sql/db/db.go b/content/sql/db/db.go index c7949fac..b9e29ce5 100644 --- a/content/sql/db/db.go +++ b/content/sql/db/db.go @@ -15,7 +15,7 @@ type DB struct { } var ( - dbVersion = 1 + dbVersion = 2 errAlreadyFrozen = errors.New("DB already frozen") errNotFrozen = errors.New("DB not frozen") diff --git a/content/sql/db/postgres/helper.go b/content/sql/db/postgres/helper.go index 869dcaca..82c708ef 100644 --- a/content/sql/db/postgres/helper.go +++ b/content/sql/db/postgres/helper.go @@ -1,6 +1,8 @@ package postgres import ( + "fmt" + "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/urandom/readeef/content/sql/db" @@ -38,6 +40,45 @@ func (h Helper) CreateWithId(tx *sqlx.Tx, name string, args ...interface{}) (int return id, nil } +func (h Helper) Upgrade(db *db.DB, old, new int) error { + for old < new { + switch old { + case 1: + if err := upgrade1to2(db); err != nil { + return fmt.Errorf("Error upgrading db from %d to %d: %v\n", old, new, err) + } + } + old++ + } + + return nil +} + +func upgrade1to2(db *db.DB) error { + tx, err := db.Beginx() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Exec(upgrade1To2MergeReadAndFav) + if err != nil { + return err + } + + _, err = tx.Exec("DROP TABLE users_articles_read") + if err != nil { + return err + } + + _, err = tx.Exec("DROP TABLE users_articles_fav") + if err != nil { + return err + } + + return tx.Commit() +} + func init() { helper := &Helper{Helper: base.NewHelper()} @@ -61,5 +102,15 @@ FROM feeds f, users_feeds_tags uft WHERE f.id = uft.feed_id AND uft.user_login = $1 AND uft.tag = $2 ORDER BY f.title COLLATE "default" +` + + upgrade1To2MergeReadAndFav = ` +INSERT INTO users_articles_states +SELECT COALESCE(ar.user_login, af.user_login), COALESCE(ar.article_id, af.article_id), + CASE WHEN ar.article_id IS NULL THEN 'f'::BOOLEAN ELSE 't'::BOOLEAN END AS read, + CASE WHEN af.article_id IS NULL THEN 'f'::BOOLEAN ELSE 't'::BOOLEAN END AS favorite +FROM users_articles_read ar FULL OUTER JOIN users_articles_fav af + ON ar.article_id = af.article_id AND ar.user_login = af.user_login +ORDER BY ar.article_id ` ) diff --git a/content/sql/db/postgres/init.go b/content/sql/db/postgres/init.go index a6598bed..2d5cf37a 100644 --- a/content/sql/db/postgres/init.go +++ b/content/sql/db/postgres/init.go @@ -67,17 +67,11 @@ CREATE TABLE IF NOT EXISTS users_feeds_tags ( PRIMARY KEY(user_login, feed_id, tag), FOREIGN KEY(user_login, feed_id) REFERENCES users_feeds(user_login, feed_id) ON DELETE CASCADE )`, ` -CREATE TABLE IF NOT EXISTS users_articles_read ( - user_login TEXT, - article_id BIGINT, - - PRIMARY KEY(user_login, article_id), - FOREIGN KEY(user_login) REFERENCES users(login) ON DELETE CASCADE, - FOREIGN KEY(article_id) REFERENCES articles(id) ON DELETE CASCADE -)`, ` -CREATE TABLE IF NOT EXISTS users_articles_fav ( +CREATE TABLE IF NOT EXISTS users_articles_states ( user_login TEXT, article_id BIGINT, + read BOOLEAN DEFAULT 'f', + favorite BOOLEAN DEFAULT 'f', PRIMARY KEY(user_login, article_id), FOREIGN KEY(user_login) REFERENCES users(login) ON DELETE CASCADE, diff --git a/content/sql/db/sqlite3/helper.go b/content/sql/db/sqlite3/helper.go index 8f3c09f1..ddadba58 100644 --- a/content/sql/db/sqlite3/helper.go +++ b/content/sql/db/sqlite3/helper.go @@ -1,6 +1,8 @@ package sqlite3 import ( + "fmt" + "github.com/urandom/readeef/content/sql/db" "github.com/urandom/readeef/content/sql/db/base" ) @@ -13,12 +15,52 @@ func (h Helper) InitSQL() []string { return initSQL } +func (h Helper) Upgrade(db *db.DB, old, new int) error { + for old < new { + switch old { + case 1: + if err := upgrade1to2(db); err != nil { + return fmt.Errorf("Error upgrading db from %d to %d: %v\n", old, new, err) + } + } + old++ + } + + return nil +} + +func upgrade1to2(db *db.DB) error { + tx, err := db.Beginx() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Exec(upgrade1To2MergeReadAndFav) + if err != nil { + return err + } + + _, err = tx.Exec("DROP TABLE users_articles_read") + if err != nil { + return err + } + + _, err = tx.Exec("DROP TABLE users_articles_fav") + if err != nil { + return err + } + + return tx.Commit() +} + func init() { helper := &Helper{Helper: base.NewHelper()} helper.Set("get_user_feeds", getUserFeeds) helper.Set("get_user_tag_feeds", getUserTagFeeds) helper.Set("get_latest_feed_articles", getLatestFeedArticles) + helper.Set("create_user_article_state", createUserArticleState) db.Register("sqlite3", helper) } @@ -43,5 +85,26 @@ ORDER BY f.title COLLATE NOCASE SELECT a.feed_id, a.id, a.title, a.description, a.link, a.date, a.guid FROM articles a WHERE a.feed_id = $1 AND a.date > DATE('NOW', '-5 days') +` + + createUserArticleState = ` +INSERT INTO users_articles_states(user_login, article_id, read, favorite) + SELECT $1, $2, $3, $4 EXCEPT + SELECT user_login, article_id, CAST($3 AS INTEGER), CAST($4 AS INTEGER) + FROM users_articles_states WHERE user_login = $1 AND article_id = $2 +` + upgrade1To2MergeReadAndFav = ` +INSERT INTO users_articles_states +SELECT ar.user_login, ar.article_id, 1 as read, + CASE WHEN af.article_id IS NULL THEN 0 ELSE 1 END AS favorite +FROM users_articles_read ar LEFT OUTER JOIN users_articles_fav af + ON ar.article_id = af.article_id AND ar.user_login = af.user_login +UNION ALL +SELECT af.user_login, af.article_id, + CASE WHEN ar.article_id IS NULL THEN 0 ELSE 1 END AS read, 1 as favorite +FROM users_articles_fav af LEFT OUTER JOIN users_articles_read ar + ON af.user_login = ar.user_login AND af.article_id = ar.article_id +WHERE ar.article_id IS NULL +ORDER BY ar.article_id, af.article_id ` ) diff --git a/content/sql/db/sqlite3/init.go b/content/sql/db/sqlite3/init.go index b8fe4831..232b3873 100644 --- a/content/sql/db/sqlite3/init.go +++ b/content/sql/db/sqlite3/init.go @@ -69,17 +69,11 @@ CREATE TABLE IF NOT EXISTS users_feeds_tags ( PRIMARY KEY(user_login, feed_id, tag), FOREIGN KEY(user_login, feed_id) REFERENCES users_feeds(user_login, feed_id) ON DELETE CASCADE )`, ` -CREATE TABLE IF NOT EXISTS users_articles_read ( +CREATE TABLE IF NOT EXISTS users_articles_states ( user_login TEXT, - article_id INTEGER, - - PRIMARY KEY(user_login, article_id), - FOREIGN KEY(user_login) REFERENCES users(login) ON DELETE CASCADE, - FOREIGN KEY(article_id) REFERENCES articles(id) ON DELETE CASCADE -)`, ` -CREATE TABLE IF NOT EXISTS users_articles_fav ( - user_login TEXT, - article_id INTEGER, + article_id BIGINT, + read INTEGER DEFAULT 0, + favorite INTEGER DEFAULT 0, PRIMARY KEY(user_login, article_id), FOREIGN KEY(user_login) REFERENCES users(login) ON DELETE CASCADE, diff --git a/content/sql/feed.go b/content/sql/feed.go index 96ed1c96..e2d0548b 100644 --- a/content/sql/feed.go +++ b/content/sql/feed.go @@ -440,7 +440,7 @@ func (uf *UserFeed) UnreadArticles(paging ...int) (ua []content.UserArticle) { uf.logger.Infof("Getting unread articles for feed %d\n", id) - articles := uf.getArticles("ar.article_id IS NULL", "", paging...) + articles := uf.getArticles("uas.article_id IS NULL OR NOT uas.read", "", paging...) ua = make([]content.UserArticle, len(articles)) for i := range articles { ua[i] = articles[i] @@ -497,7 +497,7 @@ func (uf *UserFeed) ReadBefore(date time.Time, read bool) { } defer tx.Rollback() - stmt, err := tx.Preparex(uf.db.SQL("delete_all_users_articles_read_by_feed_date")) + stmt, err := tx.Preparex(uf.db.SQL("create_missing_user_article_state_by_feed_date")) if err != nil { uf.Err(err) return @@ -510,22 +510,23 @@ func (uf *UserFeed) ReadBefore(date time.Time, read bool) { return } - if read { - stmt, err = tx.Preparex(uf.db.SQL("create_all_users_articles_read_by_feed_date")) - if err != nil { - uf.Err(err) - return - } - defer stmt.Close() + stmt, err = tx.Preparex(uf.db.SQL("update_all_user_article_state_by_feed_date")) + if err != nil { + uf.Err(err) + return + } + defer stmt.Close() - _, err = stmt.Exec(login, id, date) - if err != nil { - uf.Err(err) - return - } + _, err = stmt.Exec(read, login, id, date) + if err != nil { + uf.Err(err) + return + } + + if err = tx.Commit(); err != nil { + uf.Err(err) } - tx.Commit() } func (uf *UserFeed) ScoredArticles(from, to time.Time, paging ...int) (ua []content.UserArticle) { @@ -721,7 +722,6 @@ func (tf *TaggedFeed) UpdateTags() { tf.Err(err) return } - fmt.Println(tags[i]) _, err = stmt.Exec(login, id, tags[i].Value()) if err != nil { diff --git a/content/sql/tag.go b/content/sql/tag.go index 10492f5f..ac2405a5 100644 --- a/content/sql/tag.go +++ b/content/sql/tag.go @@ -89,7 +89,7 @@ func (t *Tag) UnreadArticles(paging ...int) (ua []content.UserArticle) { articles := getArticles(t.User(), t.db, t.logger, t, "", "INNER JOIN users_feeds_tags uft ON uft.feed_id = uf.feed_id AND uft.user_login = uf.user_login", - "uft.tag = $2 AND ar.article_id IS NULL", "", []interface{}{t.String()}, paging...) + "uft.tag = $2 AND uas.article_id IS NULL OR NOT uas.read", "", []interface{}{t.String()}, paging...) ua = make([]content.UserArticle, len(articles)) for i := range articles { @@ -131,7 +131,8 @@ func (t *Tag) ReadBefore(date time.Time, read bool) { return } - t.logger.Infof("Marking articles for tag %s before %v as read\n", t, date) + login := t.User().Data().Login + t.logger.Infof("Marking user %s articles for tag %s before %v as read\n", login, t, date) tx, err := t.db.Beginx() if err != nil { @@ -140,7 +141,7 @@ func (t *Tag) ReadBefore(date time.Time, read bool) { } defer tx.Rollback() - stmt, err := tx.Preparex(t.db.SQL("delete_all_user_tag_articles_read_by_date")) + stmt, err := tx.Preparex(t.db.SQL("create_missing_user_article_state_by_tag_date")) if err != nil { t.Err(err) @@ -148,29 +149,29 @@ func (t *Tag) ReadBefore(date time.Time, read bool) { } defer stmt.Close() - _, err = stmt.Exec(t.User().Data().Login, t.String(), date) + _, err = stmt.Exec(login, t.String(), date) if err != nil { t.Err(err) return } - if read { - stmt, err = tx.Preparex(t.db.SQL("create_all_user_tag_articles_read_by_date")) + stmt, err = tx.Preparex(t.db.SQL("update_all_user_article_state_by_tag_date")) - if err != nil { - t.Err(err) - return - } - defer stmt.Close() + if err != nil { + t.Err(err) + return + } + defer stmt.Close() - _, err = stmt.Exec(t.User().Data().Login, t.String(), date) - if err != nil { - t.Err(err) - return - } + _, err = stmt.Exec(read, login, t.String(), date) + if err != nil { + t.Err(err) + return } - tx.Commit() + if err = tx.Commit(); err != nil { + t.Err(err) + } } func (t *Tag) ScoredArticles(from, to time.Time, paging ...int) (ua []content.UserArticle) { diff --git a/content/sql/test/article_test.go b/content/sql/test/article_test.go index a186f9f2..2f2a0c83 100644 --- a/content/sql/test/article_test.go +++ b/content/sql/test/article_test.go @@ -28,7 +28,7 @@ func TestScoredArticle(t *testing.T) { asc1 := createArticleScores(data.ArticleScores{ArticleId: id1, Score1: 2, Score2: 2}) asc2 := createArticleScores(data.ArticleScores{ArticleId: id3, Score1: 1, Score2: 3}) - sa := repo.ScoredArticle() + sa := repo.Article() sa.Data(data.Article{Id: id1}) tests.CheckInt64(t, asc1.Calculate(), sa.Scores().Calculate()) @@ -96,7 +96,7 @@ func createUserArticle(u content.User, d data.Article) (ua content.Article) { } func createScoredArticle(u content.User, d data.Article) (sa content.Article) { - sa = repo.ScoredArticle() + sa = repo.Article() sa.Data(d) return sa diff --git a/content/sql/test/main_postgres_test.go b/content/sql/test/main_postgres_test.go index 4fed25a3..59767873 100644 --- a/content/sql/test/main_postgres_test.go +++ b/content/sql/test/main_postgres_test.go @@ -20,8 +20,7 @@ func TestMain(m *testing.M) { db.Exec("TRUNCATE feeds CASCADE") db.Exec("TRUNCATE hubbub_subscriptions CASCADE") db.Exec("TRUNCATE users CASCADE") - db.Exec("TRUNCATE users_articles_fav CASCADE") - db.Exec("TRUNCATE users_articles_read CASCADE") + db.Exec("TRUNCATE users_articles_states CASCADE") db.Exec("TRUNCATE users_feeds CASCADE") db.Exec("TRUNCATE users_feeds_tags CASCADE") diff --git a/content/sql/test/main_sqlite3_test.go b/content/sql/test/main_sqlite3_test.go index 5f5d32e7..66f5579c 100644 --- a/content/sql/test/main_sqlite3_test.go +++ b/content/sql/test/main_sqlite3_test.go @@ -21,8 +21,7 @@ func TestMain(m *testing.M) { db.Exec("DELETE FROM feeds") db.Exec("DELETE FROM hubbub_subscriptions") db.Exec("DELETE FROM users") - db.Exec("DELETE FROM users_articles_fav") - db.Exec("DELETE FROM users_articles_read") + db.Exec("DELETE FROM users_articles_states") db.Exec("DELETE FROM users_feeds") db.Exec("DELETE FROM users_feeds_tags") diff --git a/content/sql/user.go b/content/sql/user.go index f3d2ceb5..0211af41 100644 --- a/content/sql/user.go +++ b/content/sql/user.go @@ -443,7 +443,7 @@ func (u *User) UnreadArticles(paging ...int) (ua []content.UserArticle) { login := u.Data().Login u.logger.Infof("Getting unread articles for paging %q and user %s\n", paging, login) - articles := getArticles(u, u.db, u.logger, u, "", "", "ar.article_id IS NULL", "", nil, paging...) + articles := getArticles(u, u.db, u.logger, u, "", "", "uas.article_id IS NULL OR NOT uas.read", "", nil, paging...) ua = make([]content.UserArticle, len(articles)) for i := range articles { ua[i] = articles[i] @@ -518,7 +518,7 @@ func (u *User) FavoriteArticles(paging ...int) (ua []content.UserArticle) { login := u.Data().Login u.logger.Infof("Getting favorite articles for paging %q and user %s\n", paging, login) - articles := getArticles(u, u.db, u.logger, u, "", "", "af.article_id IS NOT NULL", "", nil, paging...) + articles := getArticles(u, u.db, u.logger, u, "", "", "uas.favorite", "", nil, paging...) ua = make([]content.UserArticle, len(articles)) for i := range articles { ua[i] = articles[i] @@ -566,7 +566,7 @@ func (u *User) ReadBefore(date time.Time, read bool) { } defer tx.Rollback() - stmt, err := tx.Preparex(u.db.SQL("delete_all_user_articles_read_by_date")) + stmt, err := tx.Preparex(u.db.SQL("create_missing_user_article_state_by_date")) if err != nil { u.Err(err) return @@ -579,23 +579,23 @@ func (u *User) ReadBefore(date time.Time, read bool) { return } - if read { - stmt, err = tx.Preparex(u.db.SQL("create_all_user_articles_read_by_date")) + stmt, err = tx.Preparex(u.db.SQL("update_all_user_article_state_by_date")) - if err != nil { - u.Err(err) - return - } - defer stmt.Close() + if err != nil { + u.Err(err) + return + } + defer stmt.Close() - _, err = stmt.Exec(login, date) - if err != nil { - u.Err(err) - return - } + _, err = stmt.Exec(read, login, date) + if err != nil { + u.Err(err) + return } - tx.Commit() + if err = tx.Commit(); err != nil { + u.Err(err) + } } func (u *User) ReadAfter(date time.Time, read bool) { @@ -618,7 +618,7 @@ func (u *User) ReadAfter(date time.Time, read bool) { } defer tx.Rollback() - stmt, err := tx.Preparex(u.db.SQL("delete_newer_user_articles_read_by_date")) + stmt, err := tx.Preparex(u.db.SQL("create_newer_missing_user_article_state_by_date")) if err != nil { u.Err(err) @@ -632,25 +632,23 @@ func (u *User) ReadAfter(date time.Time, read bool) { return } - if read { - stmt, err = tx.Preparex(u.db.SQL("create_newer_user_articles_read_by_date")) - - if err != nil { - u.Err(err) - return - } - defer stmt.Close() + stmt, err = tx.Preparex(u.db.SQL("update_all_newer_user_article_state_by_date")) - _, err := stmt.Exec(login, date) - if err != nil { - u.Err(err) - return - } + if err != nil { + u.Err(err) + return } + defer stmt.Close() - tx.Commit() + _, err = stmt.Exec(read, login, date) + if err != nil { + u.Err(err) + return + } - return + if err = tx.Commit(); err != nil { + u.Err(err) + } } func (u *User) ScoredArticles(from, to time.Time, paging ...int) (ua []content.UserArticle) {