Skip to content

Commit f9b5751

Browse files
authored
feat: Use transactions when storing feeds to local database (#157)
This updates the store interface with a BeginBatch() method that causes subsequent calls to UpsertItem() to happen inside a transaction (until EndBatch() is called). We then modify fetchAllFeeds to use the updated API to wrap its outer loop in a transaction. In a small test (6 feeds, about 2500 items) this reduced the time required to run `refresh` from 1 minutes to 1 second. That's a 98% improvement! Closes: #156
1 parent 045a63b commit f9b5751

File tree

2 files changed

+53
-6
lines changed

2 files changed

+53
-6
lines changed

internal/commands/commands.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ func (c Commands) fetchAllFeeds() ([]store.Item, []ErrorItem, error) {
151151
close(ch)
152152
}()
153153

154+
err := c.store.BeginBatch()
155+
if err != nil {
156+
return items, errorItems, fmt.Errorf("fetchAllFeeds: failed to begin batch: %w", err)
157+
}
158+
defer c.store.EndBatch()
159+
154160
for result := range ch {
155161
if result.err != nil {
156162
errorItems = append(errorItems, ErrorItem{FeedURL: result.url, Err: result.err})

internal/store/store.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ func (i Item) Read() bool {
3434

3535
type Store interface {
3636
UpsertItem(item Item) error
37+
BeginBatch() error
38+
EndBatch() error
3739
GetAllItems(ordering string) ([]Item, error)
3840
GetItemByID(ID int) (Item, error)
3941
GetAllFeedURLs() ([]string, error)
@@ -45,8 +47,9 @@ type Store interface {
4547
}
4648

4749
type SQLiteStore struct {
48-
path string
49-
db *sql.DB
50+
path string
51+
db *sql.DB
52+
batch *sql.Tx
5053
}
5154

5255
func NewSQLiteStore(basePath string, dbName string) (*SQLiteStore, error) {
@@ -148,8 +151,46 @@ func runMigrations(db *sql.DB) (err error) {
148151
return err
149152
}
150153

151-
func (sls SQLiteStore) UpsertItem(item Item) error {
152-
stmt, err := sls.db.Prepare(`select count(id), id from items where feedurl = ? and title = ?;`)
154+
// Begin a transaction. UpsertItem will use this transaction until
155+
// client code calls EndBatch().
156+
func (sls *SQLiteStore) BeginBatch() error {
157+
tx, err := sls.db.Begin()
158+
if err != nil {
159+
return fmt.Errorf("BeginBatch: %w", err)
160+
}
161+
sls.batch = tx
162+
return nil
163+
}
164+
165+
// Commit a transaction. This commits any changes made since BeginBatch()
166+
// and reset sls.batch to nil so that subsequent calls to UpsertItem()
167+
// will not use a transaction.
168+
func (sls *SQLiteStore) EndBatch() error {
169+
if sls.batch == nil {
170+
return nil
171+
}
172+
err := sls.batch.Commit()
173+
sls.batch = nil
174+
if err != nil {
175+
return fmt.Errorf("EndBatch: %w", err)
176+
}
177+
return nil
178+
}
179+
180+
func (sls *SQLiteStore) UpsertItem(item Item) error {
181+
if sls.batch != nil {
182+
return sls.upsertItem(sls.batch, item)
183+
}
184+
return sls.upsertItem(sls.db, item)
185+
}
186+
187+
// This interface is implemented by both sql.DB and by sql.Tx
188+
type statementPreparer interface {
189+
Prepare(query string) (*sql.Stmt, error)
190+
}
191+
192+
func (sls *SQLiteStore) upsertItem(db statementPreparer, item Item) error {
193+
stmt, err := db.Prepare(`select count(id), id from items where feedurl = ? and title = ?;`)
153194
if err != nil {
154195
return fmt.Errorf("sqlite.go: could not prepare query: %w", err)
155196
}
@@ -162,7 +203,7 @@ func (sls SQLiteStore) UpsertItem(item Item) error {
162203
}
163204

164205
if count == 0 {
165-
stmt, err = sls.db.Prepare(`insert into items (feedurl, link, title, content, author, publishedat, createdat, updatedat) values (?, ?, ?, ?, ?, ?, ?, ?)`)
206+
stmt, err = db.Prepare(`insert into items (feedurl, link, title, content, author, publishedat, createdat, updatedat) values (?, ?, ?, ?, ?, ?, ?, ?)`)
166207
if err != nil {
167208
return fmt.Errorf("sqlite.go: could not prepare query: %w", err)
168209
}
@@ -172,7 +213,7 @@ func (sls SQLiteStore) UpsertItem(item Item) error {
172213
return fmt.Errorf("sqlite.go: Upsert failed: %w", err)
173214
}
174215
} else {
175-
stmt, err = sls.db.Prepare(`update items set content = ?, updatedat = ? where id = ?`)
216+
stmt, err = db.Prepare(`update items set content = ?, updatedat = ? where id = ?`)
176217
if err != nil {
177218
return fmt.Errorf("sqlite.go: could not prepare query: %w", err)
178219
}

0 commit comments

Comments
 (0)