refactor feedstate + swap implementation

This commit is contained in:
nkanaev
2026-05-18 20:06:41 +01:00
parent 7553824520
commit 85f3956b24
9 changed files with 101 additions and 167 deletions

View File

@@ -30,7 +30,7 @@ func (s *Storage) CreateFeed(params CreateFeedParams) *Feed {
title = params.FeedLink
}
row := s.db.QueryRow(`
insert into feeds (title, description, link, feed_link, folder_id)
insert into feeds (title, description, link, feed_link, folder_id)
values (:title, :description, :link, :feed_link, :folder_id)
on conflict (feed_link) do update set folder_id = :folder_id
returning id`,
@@ -185,42 +185,3 @@ func (s *Storage) GetFeed(id int64) *Feed {
}
return &f
}
func (s *Storage) ResetFeedErrors() {
if _, err := s.db.Exec(`delete from feed_errors`); err != nil {
log.Print(err)
}
}
func (s *Storage) SetFeedError(feedID int64, lastError error) {
_, err := s.db.Exec(`
insert into feed_errors (feed_id, error)
values (:feed_id, :error)
on conflict (feed_id) do update set error = excluded.error`,
sql.Named("feed_id", feedID),
sql.Named("error", lastError.Error()),
)
if err != nil {
log.Print(err)
}
}
func (s *Storage) GetFeedErrors() map[int64]string {
errors := make(map[int64]string)
rows, err := s.db.Query(`select feed_id, error from feed_errors`)
if err != nil {
log.Print(err)
return errors
}
for rows.Next() {
var id int64
var error string
if err = rows.Scan(&id, &error); err != nil {
log.Print(err)
}
errors[id] = error
}
return errors
}

View File

@@ -8,14 +8,19 @@ import (
type FeedState struct {
FeedID int64
LastRefreshed time.Time
LastError *string
LastError string
HTTPLastModified string
HTTPEtag string
}
func (s *Storage) ListFeedStates() ([]FeedState, error) {
rows, err := s.db.Query(`
select feed_id, last_refreshed, last_modified, etag, last_error
select
feed_id
, last_refreshed
, last_error
, http_lmod
, http_etag
from feed_states
`)
if err != nil {
@@ -29,9 +34,9 @@ func (s *Storage) ListFeedStates() ([]FeedState, error) {
err := rows.Scan(
&state.FeedID,
&state.LastRefreshed,
&state.LastError,
&state.HTTPLastModified,
&state.HTTPEtag,
&state.LastError,
)
if err != nil {
return nil, err
@@ -44,14 +49,19 @@ func (s *Storage) ListFeedStates() ([]FeedState, error) {
func (s *Storage) GetFeedState(feedID int64) (*FeedState, error) {
var state FeedState
err := s.db.QueryRow(`
select feed_id, last_refreshed, last_modified, etag, last_error
select
feed_id
, last_refreshed
, last_error
, http_lmod
, http_etag
from feed_states where feed_id = :id
`, sql.Named("id", feedID)).Scan(
&state.FeedID,
&state.LastRefreshed,
&state.LastError,
&state.HTTPLastModified,
&state.HTTPEtag,
&state.LastError,
)
if err == sql.ErrNoRows {
return nil, nil
@@ -79,28 +89,28 @@ func (s *Storage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (b
insert into feed_states (
feed_id
, last_refreshed
, last_modified
, etag
, last_error
, http_lmod
, http_etag
)
values (
:id
, coalesce(:refreshed, 0)
, coalesce(:last_modified, '')
, coalesce(:etag, '')
, coalesce(:last_refreshed, 0)
, coalesce(:last_error, '')
, coalesce(:http_lmod, '')
, coalesce(:http_etag, '')
)
on conflict (feed_id) do update set
last_refreshed = coalesce(:refreshed, last_refreshed),
last_modified = coalesce(:last_modified, last_modified),
etag = coalesce(:etag, etag),
last_error = coalesce(:last_error, last_error)
last_refreshed = coalesce(:last_refreshed, last_refreshed),
last_error = coalesce(:last_error, last_modified),
http_lmod = coalesce(:http_lmod, http_lmod),
http_etag = coalesce(:http_etag, http_etag)
`,
sql.Named("id", feedID),
sql.Named("refreshed", params.LastRefreshed),
sql.Named("last_modified", params.HTTPLastModified),
sql.Named("etag", params.HTTPEtag),
sql.Named("last_refreshed", params.LastRefreshed),
sql.Named("last_error", params.LastError),
sql.Named("http_lmod", params.HTTPLastModified),
sql.Named("http_etag", params.HTTPEtag),
)
if err != nil {
return false, err

View File

@@ -39,7 +39,7 @@ func TestUpdateFeedState_Full(t *testing.T) {
if !state.LastRefreshed.Equal(now) {
t.Errorf("expected %v, got %v", now, state.LastRefreshed)
}
if state.LastError == nil || *state.LastError != errMsg {
if state.LastError != errMsg {
t.Errorf("expected %s, got %v", errMsg, state.LastError)
}
if state.HTTPLastModified != lmod {
@@ -70,7 +70,7 @@ func TestUpdateFeedState_Partial(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if state.LastError == nil || *state.LastError != newErr {
if state.LastError != newErr {
t.Errorf("expected %s, got %v", newErr, state.LastError)
}
if state.HTTPEtag != etag {
@@ -98,8 +98,8 @@ func TestUpdateFeedState_ClearError(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if state.LastError == nil || *state.LastError != "" {
t.Errorf("expected empty string error, got %v", state.LastError)
if state.LastError != "" {
t.Errorf("expected empty error string, got %v", state.LastError)
}
}
@@ -111,9 +111,8 @@ func TestListFeedStates(t *testing.T) {
f2 := s.CreateFeed(CreateFeedParams{Title: "F2", FeedLink: "L2"})
errMsg := "fail"
etag := "e"
s.UpdateFeedState(f1.Id, UpdateFeedStateParams{LastError: &errMsg})
s.UpdateFeedState(f2.Id, UpdateFeedStateParams{HTTPEtag: &etag})
s.UpdateFeedState(f2.Id, UpdateFeedStateParams{HTTPEtag: ptr("e")})
states, err := s.ListFeedStates()
if err != nil {
@@ -124,3 +123,7 @@ func TestListFeedStates(t *testing.T) {
t.Errorf("expected 2 states, got %d", len(states))
}
}
func ptr[T any](v T) *T {
return &v
}

View File

@@ -1,73 +0,0 @@
package storage
import (
"database/sql"
"log"
"time"
)
type HTTPState struct {
FeedID int64
LastRefreshed time.Time
LastModified string
Etag string
}
func (s *Storage) ListHTTPStates() map[int64]HTTPState {
result := make(map[int64]HTTPState)
rows, err := s.db.Query(`select feed_id, last_refreshed, last_modified, etag from http_states`)
if err != nil {
log.Print(err)
return result
}
for rows.Next() {
var state HTTPState
err = rows.Scan(
&state.FeedID,
&state.LastRefreshed,
&state.LastModified,
&state.Etag,
)
if err != nil {
log.Print(err)
return result
}
result[state.FeedID] = state
}
return result
}
func (s *Storage) GetHTTPState(feedID int64) *HTTPState {
row := s.db.QueryRow(`
select feed_id, last_refreshed, last_modified, etag
from http_states where feed_id = :feed_id
`, sql.Named("feed_id", feedID))
if row == nil {
return nil
}
var state HTTPState
row.Scan(
&state.FeedID,
&state.LastRefreshed,
&state.LastModified,
&state.Etag,
)
return &state
}
func (s *Storage) SetHTTPState(feedID int64, lastModified, etag string) {
_, err := s.db.Exec(`
insert into http_states (feed_id, last_modified, etag, last_refreshed)
values (:feed_id, :last_modified, :etag, datetime())
on conflict (feed_id) do update set last_modified = :last_modified, etag = :etag, last_refreshed = datetime()`,
sql.Named("feed_id", feedID),
sql.Named("last_modified", lastModified),
sql.Named("etag", etag),
)
if err != nil {
log.Print(err)
}
}

View File

@@ -350,20 +350,27 @@ func m12_remove_feed_sizes(tx *sql.Tx) error {
func m13_consolidate_feed_states(tx *sql.Tx) error {
sql := `
create table feed_states (
feed_id references feeds(id) on delete cascade unique,
last_refreshed datetime not null default 0,
last_modified string not null default '',
etag string not null default '',
last_error string not null default ''
feed_id references feeds(id) on delete cascade unique
, last_refreshed datetime not null default 0
, last_error string not null default ''
, http_lmod string not null default ''
, http_etag string not null default ''
);
insert into feed_states (feed_id, last_refreshed, last_modified, etag, last_error)
insert into feed_states (
feed_id
, last_refreshed
, last_error
, http_lmod
, http_etag
)
select
f.id,
coalesce(h.last_refreshed, 0),
coalesce(e.error, '')
coalesce(h.last_modified, ''),
coalesce(h.etag, ''),
coalesce(e.error, '')
from feeds f
left join http_states h on f.id = h.feed_id
left join feed_errors e on f.id = e.feed_id