feat(store): seen-set read state with floor baseline and compaction
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package store
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// EnsureFolderBaseline initialises folder_state on first contact, or resets it
|
||||
// when the server's UIDVALIDITY differs from what we stored.
|
||||
func (s *Store) EnsureFolderBaseline(account, folder string, uidvalidity, maxUID uint32) error {
|
||||
a, err := s.GetAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
curUIDValidity uint32
|
||||
haveRow bool
|
||||
)
|
||||
row := s.db.QueryRow(
|
||||
"SELECT uidvalidity FROM folder_state WHERE account_id=? AND folder=?", a.ID, folder)
|
||||
switch err := row.Scan(&curUIDValidity); err {
|
||||
case nil:
|
||||
haveRow = true
|
||||
case sql.ErrNoRows:
|
||||
haveRow = false
|
||||
default:
|
||||
return err
|
||||
}
|
||||
if haveRow && curUIDValidity == uidvalidity {
|
||||
return nil // already baselined, same validity
|
||||
}
|
||||
|
||||
floor := maxUID
|
||||
if a.ProcessBacklog {
|
||||
floor = 0
|
||||
}
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec("DELETE FROM acked WHERE account_id=? AND folder=?", a.ID, folder); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`
|
||||
INSERT INTO folder_state(account_id,folder,uidvalidity,floor_uid)
|
||||
VALUES(?,?,?,?)
|
||||
ON CONFLICT(account_id,folder) DO UPDATE SET uidvalidity=excluded.uidvalidity, floor_uid=excluded.floor_uid`,
|
||||
a.ID, folder, uidvalidity, floor); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Store) floor(account, folder string) (uint32, error) {
|
||||
id, err := s.accountID(account)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var f uint32
|
||||
err = s.db.QueryRow(
|
||||
"SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// IsNew reports whether uid is unread: above the floor and not acked.
|
||||
func (s *Store) IsNew(account, folder string, uid uint32) (bool, error) {
|
||||
id, err := s.accountID(account)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var floor uint32
|
||||
if err := s.db.QueryRow(
|
||||
"SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&floor); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if uid <= floor {
|
||||
return false, nil
|
||||
}
|
||||
var one int
|
||||
err = s.db.QueryRow(
|
||||
"SELECT 1 FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, uid).Scan(&one)
|
||||
if err == sql.ErrNoRows {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil // present in acked
|
||||
}
|
||||
|
||||
// FilterNew returns the subset of uids that are new, preserving order.
|
||||
func (s *Store) FilterNew(account, folder string, uids []uint32) ([]uint32, error) {
|
||||
out := make([]uint32, 0, len(uids))
|
||||
for _, u := range uids {
|
||||
n, err := s.IsNew(account, folder, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Ack records uids as processed (ignoring any at or below the floor) then compacts.
|
||||
func (s *Store) Ack(account, folder string, uidvalidity uint32, uids ...uint32) error {
|
||||
id, err := s.accountID(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var floor uint32
|
||||
if err := tx.QueryRow(
|
||||
"SELECT floor_uid FROM folder_state WHERE account_id=? AND folder=?", id, folder).Scan(&floor); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, u := range uids {
|
||||
if u <= floor {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
"INSERT OR IGNORE INTO acked(account_id,folder,uidvalidity,uid) VALUES(?,?,?,?)",
|
||||
id, folder, uidvalidity, u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Compact: while floor+1 is acked, advance floor and drop that row.
|
||||
for {
|
||||
next := floor + 1
|
||||
var present int
|
||||
err := tx.QueryRow(
|
||||
"SELECT 1 FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, next).Scan(&present)
|
||||
if err == sql.ErrNoRows {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
"DELETE FROM acked WHERE account_id=? AND folder=? AND uid=?", id, folder, next); err != nil {
|
||||
return err
|
||||
}
|
||||
floor = next
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
"UPDATE folder_state SET floor_uid=? WHERE account_id=? AND folder=?", floor, id, folder); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
Reference in New Issue
Block a user