diff options
Diffstat (limited to 'internal/db/bundb')
-rw-r--r-- | internal/db/bundb/account.go | 30 | ||||
-rw-r--r-- | internal/db/bundb/bundb.go | 45 | ||||
-rw-r--r-- | internal/db/bundb/conn.go | 41 | ||||
-rw-r--r-- | internal/db/bundb/domain.go | 1 | ||||
-rw-r--r-- | internal/db/bundb/hook.go (renamed from internal/db/bundb/trace.go) | 46 | ||||
-rw-r--r-- | internal/db/bundb/mention.go | 46 | ||||
-rw-r--r-- | internal/db/bundb/notification.go | 95 | ||||
-rw-r--r-- | internal/db/bundb/status.go | 22 | ||||
-rw-r--r-- | internal/db/bundb/timeline.go | 136 |
9 files changed, 236 insertions, 226 deletions
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 30510bb8e..cf02ad100 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -35,8 +35,9 @@ import ( ) type accountDB struct { - conn *DBConn - cache *cache.AccountCache + conn *DBConn + cache *cache.AccountCache + status *statusDB } func (a *accountDB) newAccountQ(account *gtsmodel.Account) *bun.SelectQuery { @@ -232,11 +233,12 @@ func (a *accountDB) CountAccountStatuses(ctx context.Context, accountID string) } func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) { - statuses := []*gtsmodel.Status{} + statusIDs := []string{} q := a.conn. NewSelect(). - Model(&statuses). + Table("statuses"). + Column("id"). Order("id DESC") if accountID != "" { @@ -295,14 +297,30 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li q = q.Where("visibility = ?", gtsmodel.VisibilityPublic) } - if err := q.Scan(ctx); err != nil { + if err := q.Scan(ctx, &statusIDs); err != nil { return nil, a.conn.ProcessError(err) } - if len(statuses) == 0 { + // Catch case of no statuses early + if len(statusIDs) == 0 { return nil, db.ErrNoEntries } + // Allocate return slice (will be at most len statusIDS) + statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) + + for _, id := range statusIDs { + // Fetch from status from database by ID + status, err := a.status.GetStatusByID(ctx, id) + if err != nil { + logrus.Errorf("GetAccountStatuses: error getting status %q: %v", id, err) + continue + } + + // Append to return slice + statuses = append(statuses, status) + } + return statuses, nil } diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index 6328ca34f..d92318afd 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -31,7 +31,6 @@ import ( "strings" "time" - "github.com/ReneKroon/ttlcache" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/stdlib" "github.com/sirupsen/logrus" @@ -46,6 +45,7 @@ import ( "github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/migrate" + grufcache "codeberg.org/gruf/go-cache/v2" "modernc.org/sqlite" ) @@ -136,11 +136,8 @@ func NewBunDBService(ctx context.Context) (db.DB, error) { return nil, fmt.Errorf("database type %s not supported for bundb", dbType) } - // add a hook to log queries and the time they take - // only do this for logging where performance isn't 1st concern - if logrus.GetLevel() >= logrus.DebugLevel && config.GetLogDbQueries() { - conn.DB.AddQueryHook(newDebugQueryHook()) - } + // Add database query hook + conn.DB.AddQueryHook(queryHook{}) // table registration is needed for many-to-many, see: // https://bun.uptrace.dev/orm/many-to-many-relation/ @@ -155,7 +152,27 @@ func NewBunDBService(ctx context.Context) (db.DB, error) { return nil, fmt.Errorf("db migration error: %s", err) } + // Create DB structs that require ptrs to each other accounts := &accountDB{conn: conn, cache: cache.NewAccountCache()} + status := &statusDB{conn: conn, cache: cache.NewStatusCache()} + timeline := &timelineDB{conn: conn} + + // Setup DB cross-referencing + accounts.status = status + status.accounts = accounts + timeline.status = status + + // Prepare mentions cache + // TODO: move into internal/cache + mentionCache := grufcache.New[string, *gtsmodel.Mention]() + mentionCache.SetTTL(time.Minute*5, false) + mentionCache.Start(time.Second * 10) + + // Prepare notifications cache + // TODO: move into internal/cache + notifCache := grufcache.New[string, *gtsmodel.Notification]() + notifCache.SetTTL(time.Minute*5, false) + notifCache.Start(time.Second * 10) ps := &bunDBService{ Account: accounts, @@ -179,11 +196,11 @@ func NewBunDBService(ctx context.Context) (db.DB, error) { }, Mention: &mentionDB{ conn: conn, - cache: ttlcache.NewCache(), + cache: mentionCache, }, Notification: ¬ificationDB{ conn: conn, - cache: ttlcache.NewCache(), + cache: notifCache, }, Relationship: &relationshipDB{ conn: conn, @@ -191,15 +208,9 @@ func NewBunDBService(ctx context.Context) (db.DB, error) { Session: &sessionDB{ conn: conn, }, - Status: &statusDB{ - conn: conn, - cache: cache.NewStatusCache(), - accounts: accounts, - }, - Timeline: &timelineDB{ - conn: conn, - }, - conn: conn, + Status: status, + Timeline: timeline, + conn: conn, } // we can confidently return this useable service now diff --git a/internal/db/bundb/conn.go b/internal/db/bundb/conn.go index baa0baeae..1c85f6f6f 100644 --- a/internal/db/bundb/conn.go +++ b/internal/db/bundb/conn.go @@ -11,13 +11,11 @@ import ( // DBConn wrapps a bun.DB conn to provide SQL-type specific additional functionality type DBConn struct { - // TODO: move *Config here, no need to be in each struct type - errProc func(error) db.Error // errProc is the SQL-type specific error processor *bun.DB // DB is the underlying bun.DB connection } -// WrapDBConn @TODO +// WrapDBConn wraps a bun DB connection to provide our own error processing dependent on DB dialect. func WrapDBConn(dbConn *bun.DB) *DBConn { var errProc func(error) db.Error switch dbConn.Dialect().Name() { @@ -36,21 +34,31 @@ func WrapDBConn(dbConn *bun.DB) *DBConn { // RunInTx wraps execution of the supplied transaction function. func (conn *DBConn) RunInTx(ctx context.Context, fn func(bun.Tx) error) db.Error { - // Acquire a new transaction - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return conn.ProcessError(err) - } + return conn.ProcessError(func() error { + // Acquire a new transaction + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return err + } - // Perform supplied transaction - if err = fn(tx); err != nil { - tx.Rollback() //nolint - return conn.ProcessError(err) - } + var done bool + + defer func() { + if !done { + _ = tx.Rollback() + } + }() + + // Perform supplied transaction + if err := fn(tx); err != nil { + return err + } - // Finally, commit transaction - err = tx.Commit() - return conn.ProcessError(err) + // Finally, commit + err = tx.Commit() + done = true + return err + }()) } // ProcessError processes an error to replace any known values with our own db.Error types, @@ -83,7 +91,6 @@ func (conn *DBConn) Exists(ctx context.Context, query *bun.SelectQuery) (bool, d // NotExists is the functional opposite of conn.Exists() func (conn *DBConn) NotExists(ctx context.Context, query *bun.SelectQuery) (bool, db.Error) { - // Simply inverse of conn.exists() exists, err := conn.Exists(ctx, query) return !exists, err } diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go index ee7fed6a9..fadb6dcf9 100644 --- a/internal/db/bundb/domain.go +++ b/internal/db/bundb/domain.go @@ -74,6 +74,5 @@ func (d *domainDB) AreURIsBlocked(ctx context.Context, uris []*url.URL) (bool, d for _, uri := range uris { domains = append(domains, uri.Hostname()) } - return d.AreDomainsBlocked(ctx, domains) } diff --git a/internal/db/bundb/trace.go b/internal/db/bundb/hook.go index 27b5e22ac..6f9935272 100644 --- a/internal/db/bundb/trace.go +++ b/internal/db/bundb/hook.go @@ -26,35 +26,33 @@ import ( "github.com/uptrace/bun" ) -func newDebugQueryHook() bun.QueryHook { - return &debugQueryHook{} -} - -// debugQueryHook implements bun.QueryHook -type debugQueryHook struct { -} +// queryHook implements bun.QueryHook +type queryHook struct{} -func (q *debugQueryHook) BeforeQuery(ctx context.Context, _ *bun.QueryEvent) context.Context { - // do nothing - return ctx +func (queryHook) BeforeQuery(ctx context.Context, _ *bun.QueryEvent) context.Context { + return ctx // do nothing } // AfterQuery logs the time taken to query, the operation (select, update, etc), and the query itself as translated by bun. -func (q *debugQueryHook) AfterQuery(_ context.Context, event *bun.QueryEvent) { - dur := time.Since(event.StartTime).Round(time.Microsecond) - l := logrus.WithFields(logrus.Fields{ - "duration": dur, - "operation": event.Operation(), - }) - - if dur > 1*time.Second { - l.Warnf("SLOW DATABASE QUERY [%s] %s", dur, event.Query) - return +func (queryHook) AfterQuery(_ context.Context, event *bun.QueryEvent) { + // Get the DB query duration + dur := time.Since(event.StartTime) + + log := func(lvl logrus.Level, msg string) { + logrus.WithFields(logrus.Fields{ + "duration": dur, + "operation": event.Operation(), + "query": event.Query, + }).Log(lvl, msg) } - if logrus.GetLevel() == logrus.TraceLevel { - l.Tracef("[%s] %s", dur, event.Query) - } else { - l.Debugf("[%s] %s", dur, event.Operation()) + switch { + // Warn on slow database queries + case dur > time.Second: + log(logrus.WarnLevel, "SLOW DATABASE QUERY") + + // On trace, we log query information + case logrus.GetLevel() == logrus.TraceLevel: + log(logrus.TraceLevel, "database query") } } diff --git a/internal/db/bundb/mention.go b/internal/db/bundb/mention.go index 1c1c25c4b..067f0d676 100644 --- a/internal/db/bundb/mention.go +++ b/internal/db/bundb/mention.go @@ -21,7 +21,8 @@ package bundb import ( "context" - "github.com/ReneKroon/ttlcache" + "codeberg.org/gruf/go-cache/v2" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/uptrace/bun" @@ -29,7 +30,7 @@ import ( type mentionDB struct { conn *DBConn - cache *ttlcache.Cache + cache cache.Cache[string, *gtsmodel.Mention] } func (m *mentionDB) newMentionQ(i interface{}) *bun.SelectQuery { @@ -41,40 +42,24 @@ func (m *mentionDB) newMentionQ(i interface{}) *bun.SelectQuery { Relation("TargetAccount") } -func (m *mentionDB) getMentionCached(id string) (*gtsmodel.Mention, bool) { - v, ok := m.cache.Get(id) - if !ok { - return nil, false - } - - mention, ok := v.(*gtsmodel.Mention) - if !ok { - panic("mention cache entry was not a mention") - } - - return mention, true -} - -func (m *mentionDB) putMentionCache(mention *gtsmodel.Mention) { - m.cache.Set(mention.ID, mention) -} - func (m *mentionDB) getMentionDB(ctx context.Context, id string) (*gtsmodel.Mention, db.Error) { - mention := >smodel.Mention{} + mention := gtsmodel.Mention{} - q := m.newMentionQ(mention). + q := m.newMentionQ(&mention). Where("mention.id = ?", id) if err := q.Scan(ctx); err != nil { return nil, m.conn.ProcessError(err) } - m.putMentionCache(mention) - return mention, nil + copy := mention + m.cache.Set(mention.ID, ©) + + return &mention, nil } func (m *mentionDB) GetMention(ctx context.Context, id string) (*gtsmodel.Mention, db.Error) { - if mention, cached := m.getMentionCached(id); cached { + if mention, ok := m.cache.Get(id); ok { return mention, nil } return m.getMentionDB(ctx, id) @@ -84,16 +69,11 @@ func (m *mentionDB) GetMentions(ctx context.Context, ids []string) ([]*gtsmodel. mentions := make([]*gtsmodel.Mention, 0, len(ids)) for _, id := range ids { - // Attempt fetch from cache - mention, cached := m.getMentionCached(id) - if cached { - mentions = append(mentions, mention) - } - // Attempt fetch from DB - mention, err := m.getMentionDB(ctx, id) + mention, err := m.GetMention(ctx, id) if err != nil { - return nil, err + logrus.Errorf("GetMentions: error getting mention %q: %v", id, err) + continue } // Append mention diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index d01bb9067..f5ea099de 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -21,37 +21,39 @@ package bundb import ( "context" - "github.com/ReneKroon/ttlcache" + "codeberg.org/gruf/go-cache/v2" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/uptrace/bun" ) type notificationDB struct { conn *DBConn - cache *ttlcache.Cache -} - -func (n *notificationDB) newNotificationQ(i interface{}) *bun.SelectQuery { - return n.conn. - NewSelect(). - Model(i). - Relation("OriginAccount"). - Relation("TargetAccount"). - Relation("Status") + cache cache.Cache[string, *gtsmodel.Notification] } func (n *notificationDB) GetNotification(ctx context.Context, id string) (*gtsmodel.Notification, db.Error) { - if notification, cached := n.getNotificationCache(id); cached { + if notification, ok := n.cache.Get(id); ok { return notification, nil } - notif := >smodel.Notification{} - err := n.getNotificationDB(ctx, id, notif) - if err != nil { - return nil, err + dst := gtsmodel.Notification{ID: id} + + q := n.conn.NewSelect(). + Model(&dst). + Relation("OriginAccount"). + Relation("TargetAccount"). + Relation("Status"). + WherePK() + + if err := q.Scan(ctx); err != nil { + return nil, n.conn.ProcessError(err) } - return notif, nil + + copy := dst + n.cache.Set(id, ©) + + return &dst, nil } func (n *notificationDB) GetNotifications(ctx context.Context, accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.Error) { @@ -61,11 +63,11 @@ func (n *notificationDB) GetNotifications(ctx context.Context, accountID string, } // Make a guess for slice size - notifications := make([]*gtsmodel.Notification, 0, limit) + notifIDs := make([]string, 0, limit) q := n.conn. NewSelect(). - Model(¬ifications). + Table("notifications"). Column("id") if maxID != "" { @@ -84,56 +86,25 @@ func (n *notificationDB) GetNotifications(ctx context.Context, accountID string, q = q.Limit(limit) } - err := q.Scan(ctx) - if err != nil { + if err := q.Scan(ctx, ¬ifIDs); err != nil { return nil, n.conn.ProcessError(err) } + notifs := make([]*gtsmodel.Notification, 0, limit) + // now we have the IDs, select the notifs one by one // reason for this is that for each notif, we can instead get it from our cache if it's cached - for i, notif := range notifications { - // Check cache for notification - nn, cached := n.getNotificationCache(notif.ID) - if cached { - notifications[i] = nn - continue - } - - // Check DB for notification - err := n.getNotificationDB(ctx, notif.ID, notif) + for _, id := range notifIDs { + // Attempt fetch from DB + notif, err := n.GetNotification(ctx, id) if err != nil { - return nil, err + logrus.Errorf("GetNotifications: error getting notification %q: %v", id, err) + continue } - } - - return notifications, nil -} - -func (n *notificationDB) getNotificationCache(id string) (*gtsmodel.Notification, bool) { - v, ok := n.cache.Get(id) - if !ok { - return nil, false - } - notif, ok := v.(*gtsmodel.Notification) - if !ok { - panic("notification cache entry was not a notification") - } - - return notif, true -} - -func (n *notificationDB) putNotificationCache(notif *gtsmodel.Notification) { - n.cache.Set(notif.ID, notif) -} - -func (n *notificationDB) getNotificationDB(ctx context.Context, id string, dst *gtsmodel.Notification) error { - q := n.newNotificationQ(dst).WherePK() - - if err := q.Scan(ctx); err != nil { - return n.conn.ProcessError(err) + // Append notification + notifs = append(notifs, notif) } - n.putNotificationCache(dst) - return nil + return notifs, nil } diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 4e670f59b..74a24ebaa 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -21,6 +21,7 @@ package bundb import ( "container/list" "context" + "database/sql" "time" "github.com/sirupsen/logrus" @@ -219,21 +220,32 @@ func (s *statusDB) GetStatusChildren(ctx context.Context, status *gtsmodel.Statu } func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) { - immediateChildren := []*gtsmodel.Status{} + childIDs := []string{} q := s.conn. NewSelect(). - Model(&immediateChildren). + Table("statuses"). + Column("id"). Where("in_reply_to_id = ?", status.ID) if minID != "" { - q = q.Where("status.id > ?", minID) + q = q.Where("id > ?", minID) } - if err := q.Scan(ctx); err != nil { + if err := q.Scan(ctx, &childIDs); err != nil { + if err != sql.ErrNoRows { + logrus.Errorf("statusChildren: error getting children for %q: %v", status.ID, err) + } return } - for _, child := range immediateChildren { + for _, id := range childIDs { + // Fetch child with ID from database + child, err := s.GetStatusByID(ctx, id) + if err != nil { + logrus.Errorf("statusChildren: error getting child status %q: %v", id, err) + continue + } + insertLoop: for e := foundStatuses.Front(); e != nil; e = e.Next() { entry, ok := e.Value.(*gtsmodel.Status) diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index ca5922532..3c0d6d7e4 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -20,55 +20,52 @@ package bundb import ( "context" - "database/sql" - "sort" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/uptrace/bun" + "golang.org/x/exp/slices" ) type timelineDB struct { - conn *DBConn + conn *DBConn + status *statusDB } func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { - // Ensure reasonable - if limit < 0 { - limit = 0 - } - // Make educated guess for slice size - statuses := make([]*gtsmodel.Status, 0, limit) + statusIDs := make([]string, 0, limit) q := t.conn. NewSelect(). - Model(&statuses) + Table("statuses"). - q = q.ColumnExpr("status.*"). + // Select only IDs from table + Column("statuses.id"). // Find out who accountID follows. - Join("LEFT JOIN follows AS f ON f.target_account_id = status.account_id"). + Join("LEFT JOIN follows ON follows.target_account_id = statuses.account_id AND follows.account_id = ?", accountID). // Sort by highest ID (newest) to lowest ID (oldest) - Order("status.id DESC") + Order("statuses.id DESC") if maxID != "" { // return only statuses LOWER (ie., older) than maxID - q = q.Where("status.id < ?", maxID) + q = q.Where("statuses.id < ?", maxID) } if sinceID != "" { // return only statuses HIGHER (ie., newer) than sinceID - q = q.Where("status.id > ?", sinceID) + q = q.Where("statuses.id > ?", sinceID) } if minID != "" { // return only statuses HIGHER (ie., newer) than minID - q = q.Where("status.id > ?", minID) + q = q.Where("statuses.id > ?", minID) } if local { // return only statuses posted by local account havers - q = q.Where("status.local = ?", local) + q = q.Where("statuses.local = ?", local) } if limit > 0 { @@ -83,15 +80,30 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI // See: https://bun.uptrace.dev/guide/queries.html#select whereGroup := func(*bun.SelectQuery) *bun.SelectQuery { return q. - WhereOr("f.account_id = ?", accountID). - WhereOr("status.account_id = ?", accountID) + WhereOr("follows.account_id = ?", accountID). + WhereOr("statuses.account_id = ?", accountID) } q = q.WhereGroup(" AND ", whereGroup) - if err := q.Scan(ctx); err != nil { + if err := q.Scan(ctx, &statusIDs); err != nil { return nil, t.conn.ProcessError(err) } + + statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) + + for _, id := range statusIDs { + // Fetch status from db for ID + status, err := t.status.GetStatusByID(ctx, id) + if err != nil { + logrus.Errorf("GetHomeTimeline: error fetching status %q: %v", id, err) + continue + } + + // Append status to slice + statuses = append(statuses, status) + } + return statuses, nil } @@ -102,40 +114,56 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, accountID string, ma } // Make educated guess for slice size - statuses := make([]*gtsmodel.Status, 0, limit) + statusIDs := make([]string, 0, limit) q := t.conn. NewSelect(). - Model(&statuses). - Where("visibility = ?", gtsmodel.VisibilityPublic). - WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id")). - WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_uri")). - WhereGroup(" AND ", whereEmptyOrNull("boost_of_id")). - Order("status.id DESC") + Table("statuses"). + Column("statuses.id"). + Where("statuses.visibility = ?", gtsmodel.VisibilityPublic). + WhereGroup(" AND ", whereEmptyOrNull("statuses.in_reply_to_id")). + WhereGroup(" AND ", whereEmptyOrNull("statuses.in_reply_to_uri")). + WhereGroup(" AND ", whereEmptyOrNull("statuses.boost_of_id")). + Order("statuses.id DESC") if maxID != "" { - q = q.Where("status.id < ?", maxID) + q = q.Where("statuses.id < ?", maxID) } if sinceID != "" { - q = q.Where("status.id > ?", sinceID) + q = q.Where("statuses.id > ?", sinceID) } if minID != "" { - q = q.Where("status.id > ?", minID) + q = q.Where("statuses.id > ?", minID) } if local { - q = q.Where("status.local = ?", local) + q = q.Where("statuses.local = ?", local) } if limit > 0 { q = q.Limit(limit) } - if err := q.Scan(ctx); err != nil { + if err := q.Scan(ctx, &statusIDs); err != nil { return nil, t.conn.ProcessError(err) } + + statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) + + for _, id := range statusIDs { + // Fetch status from db for ID + status, err := t.status.GetStatusByID(ctx, id) + if err != nil { + logrus.Errorf("GetPublicTimeline: error fetching status %q: %v", id, err) + continue + } + + // Append status to slice + statuses = append(statuses, status) + } + return statuses, nil } @@ -170,46 +198,32 @@ func (t *timelineDB) GetFavedTimeline(ctx context.Context, accountID string, max err := fq.Scan(ctx) if err != nil { - if err == sql.ErrNoRows { - return nil, "", "", db.ErrNoEntries - } - return nil, "", "", err + return nil, "", "", t.conn.ProcessError(err) } if len(faves) == 0 { return nil, "", "", db.ErrNoEntries } - // map[statusID]faveID -- we need this to sort statuses by fave ID rather than status ID - statusesFavesMap := make(map[string]string, len(faves)) - statusIDs := make([]string, 0, len(faves)) - for _, f := range faves { - statusesFavesMap[f.StatusID] = f.ID - statusIDs = append(statusIDs, f.StatusID) - } + // Sort by favourite ID rather than status ID + slices.SortFunc(faves, func(a, b *gtsmodel.StatusFave) bool { + return a.ID < b.ID + }) - statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) + statuses := make([]*gtsmodel.Status, 0, len(faves)) - err = t.conn. - NewSelect(). - Model(&statuses). - Where("id IN (?)", bun.In(statusIDs)). - Scan(ctx) - if err != nil { - return nil, "", "", t.conn.ProcessError(err) - } + for _, fave := range faves { + // Fetch status from db for corresponding favourite + status, err := t.status.GetStatusByID(ctx, fave.StatusID) + if err != nil { + logrus.Errorf("GetFavedTimeline: error fetching status for fave %q: %v", fave.ID, err) + continue + } - if len(statuses) == 0 { - return nil, "", "", db.ErrNoEntries + // Append status to slice + statuses = append(statuses, status) } - // arrange statuses by fave ID - sort.Slice(statuses, func(i int, j int) bool { - statusI := statuses[i] - statusJ := statuses[j] - return statusesFavesMap[statusI.ID] < statusesFavesMap[statusJ.ID] - }) - nextMaxID := faves[len(faves)-1].ID prevMinID := faves[0].ID return statuses, nextMaxID, prevMinID, nil |