diff options
Diffstat (limited to 'internal/db')
-rw-r--r-- | internal/db/bundb/status.go | 9 | ||||
-rw-r--r-- | internal/db/bundb/statusbookmark.go | 239 | ||||
-rw-r--r-- | internal/db/bundb/statusbookmark_test.go | 41 | ||||
-rw-r--r-- | internal/db/status.go | 3 | ||||
-rw-r--r-- | internal/db/statusbookmark.go | 15 |
5 files changed, 214 insertions, 93 deletions
diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 4bb9812dc..512730dd5 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -677,12 +677,3 @@ func (s *statusDB) getStatusBoostIDs(ctx context.Context, statusID string) ([]st return statusIDs, nil }) } - -func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) { - q := s.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). - Where("? = ?", bun.Ident("status_bookmark.status_id"), status.ID). - Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID) - return exists(ctx, q) -} diff --git a/internal/db/bundb/statusbookmark.go b/internal/db/bundb/statusbookmark.go index 73fced9c3..25cbb3e27 100644 --- a/internal/db/bundb/statusbookmark.go +++ b/internal/db/bundb/statusbookmark.go @@ -20,11 +20,15 @@ package bundb import ( "context" "errors" - "fmt" + "slices" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/uptrace/bun" ) @@ -33,52 +37,158 @@ type statusBookmarkDB struct { state *state.State } -func (s *statusBookmarkDB) GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, error) { - bookmark := new(gtsmodel.StatusBookmark) +func (s *statusBookmarkDB) GetStatusBookmarkByID(ctx context.Context, id string) (*gtsmodel.StatusBookmark, error) { + return s.getStatusBookmark( + ctx, + "ID", + func(bookmark *gtsmodel.StatusBookmark) error { + return s.db. + NewSelect(). + Model(bookmark). + Where("? = ?", bun.Ident("id"), id). + Scan(ctx) + }, + id, + ) +} - err := s.db. - NewSelect(). - Model(bookmark). - Where("? = ?", bun.Ident("status_bookmark.id"), id). - Scan(ctx) +func (s *statusBookmarkDB) GetStatusBookmark(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusBookmark, error) { + return s.getStatusBookmark( + ctx, + "AccountID,StatusID", + func(bookmark *gtsmodel.StatusBookmark) error { + return s.db. + NewSelect(). + Model(bookmark). + Where("? = ?", bun.Ident("account_id"), accountID). + Where("? = ?", bun.Ident("status_id"), statusID). + Scan(ctx) + }, + accountID, statusID, + ) +} + +func (s *statusBookmarkDB) GetStatusBookmarksByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusBookmark, error) { + // Load all input bookmark IDs via cache loader callback. + bookmarks, err := s.state.Caches.GTS.StatusBookmark.LoadIDs("ID", + ids, + func(uncached []string) ([]*gtsmodel.StatusBookmark, error) { + // Preallocate expected length of uncached bookmarks. + bookmarks := make([]*gtsmodel.StatusBookmark, 0, len(uncached)) + + // Perform database query scanning + // the remaining (uncached) bookmarks. + if err := s.db.NewSelect(). + Model(&bookmarks). + Where("? IN (?)", bun.Ident("id"), bun.In(uncached)). + Scan(ctx); err != nil { + return nil, err + } + + return bookmarks, nil + }, + ) if err != nil { return nil, err } - bookmark.Account, err = s.state.DB.GetAccountByID(ctx, bookmark.AccountID) - if err != nil { - return nil, fmt.Errorf("error getting status bookmark account %q: %w", bookmark.AccountID, err) + // Reorder the bookmarks by their + // IDs to ensure in correct order. + getID := func(b *gtsmodel.StatusBookmark) string { return b.ID } + util.OrderBy(bookmarks, ids, getID) + + // Populate all loaded bookmarks, removing those we fail + // to populate (removes needing so many later nil checks). + bookmarks = slices.DeleteFunc(bookmarks, func(bookmark *gtsmodel.StatusBookmark) bool { + if err := s.PopulateStatusBookmark(ctx, bookmark); err != nil { + log.Errorf(ctx, "error populating bookmark %s: %v", bookmark.ID, err) + return true + } + return false + }) + + return bookmarks, nil +} + +func (s *statusBookmarkDB) IsStatusBookmarked(ctx context.Context, statusID string) (bool, error) { + bookmarkIDs, err := s.getStatusBookmarkIDs(ctx, statusID) + return (len(bookmarkIDs) > 0), err +} + +func (s *statusBookmarkDB) IsStatusBookmarkedBy(ctx context.Context, accountID string, statusID string) (bool, error) { + bookmark, err := s.GetStatusBookmark(ctx, accountID, statusID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return false, err } + return (bookmark != nil), nil +} + +func (s *statusBookmarkDB) getStatusBookmark(ctx context.Context, lookup string, dbQuery func(*gtsmodel.StatusBookmark) error, keyParts ...any) (*gtsmodel.StatusBookmark, error) { + // Fetch bookmark from database cache with loader callback. + bookmark, err := s.state.Caches.GTS.StatusBookmark.LoadOne(lookup, func() (*gtsmodel.StatusBookmark, error) { + var bookmark gtsmodel.StatusBookmark + + // Not cached! Perform database query. + if err := dbQuery(&bookmark); err != nil { + return nil, err + } - bookmark.TargetAccount, err = s.state.DB.GetAccountByID(ctx, bookmark.TargetAccountID) + return &bookmark, nil + }, keyParts...) if err != nil { - return nil, fmt.Errorf("error getting status bookmark target account %q: %w", bookmark.TargetAccountID, err) + return nil, err } - bookmark.Status, err = s.state.DB.GetStatusByID(ctx, bookmark.StatusID) - if err != nil { - return nil, fmt.Errorf("error getting status bookmark status %q: %w", bookmark.StatusID, err) + if gtscontext.Barebones(ctx) { + // no need to fully populate. + return bookmark, nil + } + + // Further populate the bookmark fields where applicable. + if err := s.PopulateStatusBookmark(ctx, bookmark); err != nil { + return nil, err } return bookmark, nil } -func (s *statusBookmarkDB) GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, error) { - var id string +func (s *statusBookmarkDB) PopulateStatusBookmark(ctx context.Context, bookmark *gtsmodel.StatusBookmark) (err error) { + var errs gtserror.MultiError - q := s.db. - NewSelect(). - TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). - Column("status_bookmark.id"). - Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID). - Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID). - Limit(1) + if bookmark.Account == nil { + // Bookmark author is not set, fetch from database. + bookmark.Account, err = s.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + bookmark.AccountID, + ) + if err != nil { + errs.Appendf("error getting bookmark account %s: %w", bookmark.AccountID, err) + } + } + + if bookmark.TargetAccount == nil { + // Bookmark target account is not set, fetch from database. + bookmark.TargetAccount, err = s.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + bookmark.TargetAccountID, + ) + if err != nil { + errs.Appendf("error getting bookmark target account %s: %w", bookmark.TargetAccountID, err) + } + } - if err := q.Scan(ctx, &id); err != nil { - return "", err + if bookmark.Status == nil { + // Bookmarked status not set, fetch from database. + bookmark.Status, err = s.state.DB.GetStatusByID( + gtscontext.SetBarebones(ctx), + bookmark.StatusID, + ) + if err != nil { + errs.Appendf("error getting bookmark status %s: %w", bookmark.StatusID, err) + } } - return id, nil + return errs.Combine() } func (s *statusBookmarkDB) GetStatusBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, error) { @@ -117,38 +227,46 @@ func (s *statusBookmarkDB) GetStatusBookmarks(ctx context.Context, accountID str return nil, err } - bookmarks := make([]*gtsmodel.StatusBookmark, 0, len(ids)) + return s.GetStatusBookmarksByIDs(ctx, ids) +} - for _, id := range ids { - bookmark, err := s.GetStatusBookmark(ctx, id) - if err != nil { - log.Errorf(ctx, "error getting bookmark %q: %v", id, err) - continue +func (s *statusBookmarkDB) getStatusBookmarkIDs(ctx context.Context, statusID string) ([]string, error) { + return s.state.Caches.GTS.StatusBookmarkIDs.Load(statusID, func() ([]string, error) { + var bookmarkIDs []string + + // Bookmark IDs not cached, + // perform database query. + if err := s.db. + NewSelect(). + Table("status_bookmarks"). + Column("id").Where("? = ?", bun.Ident("status_id"), statusID). + Order("id DESC"). + Scan(ctx, &bookmarkIDs); err != nil { + return nil, err } - bookmarks = append(bookmarks, bookmark) - } - - return bookmarks, nil + return bookmarkIDs, nil + }) } -func (s *statusBookmarkDB) PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) error { - _, err := s.db. - NewInsert(). - Model(statusBookmark). - Exec(ctx) - - return err +func (s *statusBookmarkDB) PutStatusBookmark(ctx context.Context, bookmark *gtsmodel.StatusBookmark) error { + return s.state.Caches.GTS.StatusBookmark.Store(bookmark, func() error { + _, err := s.db.NewInsert().Model(bookmark).Exec(ctx) + return err + }) } -func (s *statusBookmarkDB) DeleteStatusBookmark(ctx context.Context, id string) error { +func (s *statusBookmarkDB) DeleteStatusBookmarkByID(ctx context.Context, id string) error { _, err := s.db. NewDelete(). - TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). - Where("? = ?", bun.Ident("status_bookmark.id"), id). + Table("status_bookmarks"). + Where("? = ?", bun.Ident("id"), id). Exec(ctx) - - return err + if err != nil { + return err + } + s.state.Caches.GTS.StatusBookmark.Invalidate("ID", id) + return nil } func (s *statusBookmarkDB) DeleteStatusBookmarks(ctx context.Context, targetAccountID string, originAccountID string) error { @@ -156,42 +274,43 @@ func (s *statusBookmarkDB) DeleteStatusBookmarks(ctx context.Context, targetAcco return errors.New("DeleteBookmarks: one of targetAccountID or originAccountID must be set") } - // TODO: Capture bookmark IDs in a RETURNING - // statement (when bookmarks have a cache), - // + use the IDs to invalidate cache entries. - q := s.db. NewDelete(). TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")) if targetAccountID != "" { q = q.Where("? = ?", bun.Ident("status_bookmark.target_account_id"), targetAccountID) + defer s.state.Caches.GTS.StatusBookmark.Invalidate("TargetAccountID", targetAccountID) } if originAccountID != "" { q = q.Where("? = ?", bun.Ident("status_bookmark.account_id"), originAccountID) + defer s.state.Caches.GTS.StatusBookmark.Invalidate("AccountID", originAccountID) } if _, err := q.Exec(ctx); err != nil { return err } + if targetAccountID != "" { + s.state.Caches.GTS.StatusBookmark.Invalidate("TargetAccountID", targetAccountID) + } + + if originAccountID != "" { + s.state.Caches.GTS.StatusBookmark.Invalidate("AccountID", originAccountID) + } + return nil } func (s *statusBookmarkDB) DeleteStatusBookmarksForStatus(ctx context.Context, statusID string) error { - // TODO: Capture bookmark IDs in a RETURNING - // statement (when bookmarks have a cache), - // + use the IDs to invalidate cache entries. - q := s.db. NewDelete(). TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID) - if _, err := q.Exec(ctx); err != nil { return err } - + s.state.Caches.GTS.StatusBookmark.Invalidate("StatusID", statusID) return nil } diff --git a/internal/db/bundb/statusbookmark_test.go b/internal/db/bundb/statusbookmark_test.go index 2196cff79..104b090fe 100644 --- a/internal/db/bundb/statusbookmark_test.go +++ b/internal/db/bundb/statusbookmark_test.go @@ -31,23 +31,32 @@ type StatusBookmarkTestSuite struct { BunDBStandardTestSuite } -func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDOK() { +func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkOK() { testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"] - - id, err := suite.db.GetStatusBookmarkID(context.Background(), testBookmark.AccountID, testBookmark.StatusID) - if err != nil { - suite.FailNow(err.Error()) - } - - suite.Equal(testBookmark.ID, id) + bookmark, err := suite.db.GetStatusBookmark(context.Background(), testBookmark.AccountID, testBookmark.StatusID) + suite.NoError(err) + suite.Equal(testBookmark.ID, bookmark.ID) + suite.Equal(testBookmark.AccountID, bookmark.AccountID) + suite.Equal(testBookmark.StatusID, bookmark.StatusID) } -func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDNonexisting() { - id, err := suite.db.GetStatusBookmarkID(context.Background(), "01GVAVGD06YJ2FSB5GJSMF8M2K", "01GVAVGKGR1MK9ZN7JCJFYSFZV") - suite.Empty(id) +func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkNonexisting() { + bookmark, err := suite.db.GetStatusBookmark(context.Background(), "01GVAVGD06YJ2FSB5GJSMF8M2K", "01GVAVGKGR1MK9ZN7JCJFYSFZV") + suite.Nil(bookmark) suite.ErrorIs(err, db.ErrNoEntries) } +func (suite *StatusBookmarkTestSuite) IsStatusBookmarked() { + for _, bookmark := range suite.testBookmarks { + ok, err := suite.db.IsStatusBookmarked( + context.Background(), + bookmark.StatusID, + ) + suite.NoError(err) + suite.True(ok) + } +} + func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksOriginatingFromAccount() { testAccount := suite.testAccounts["local_account_1"] @@ -105,21 +114,21 @@ func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksTargetingStatus() } } -func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmark() { +func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarkByID() { testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"] ctx := context.Background() - if err := suite.db.DeleteStatusBookmark(ctx, testBookmark.ID); err != nil { + if err := suite.db.DeleteStatusBookmarkByID(ctx, testBookmark.ID); err != nil { suite.FailNow(err.Error()) } - bookmark, err := suite.db.GetStatusBookmark(ctx, testBookmark.ID) + bookmark, err := suite.db.GetStatusBookmarkByID(ctx, testBookmark.ID) suite.ErrorIs(err, db.ErrNoEntries) suite.Nil(bookmark) } -func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarkNonExisting() { - err := suite.db.DeleteStatusBookmark(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G") +func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarkByIDNonExisting() { + err := suite.db.DeleteStatusBookmarkByID(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G") suite.NoError(err) } diff --git a/internal/db/status.go b/internal/db/status.go index 8034d39e7..88ae12a12 100644 --- a/internal/db/status.go +++ b/internal/db/status.go @@ -78,7 +78,4 @@ type Status interface { // GetStatusChildren gets the child statuses of a given status. GetStatusChildren(ctx context.Context, statusID string) ([]*gtsmodel.Status, error) - - // IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID - IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) } diff --git a/internal/db/statusbookmark.go b/internal/db/statusbookmark.go index 542e49f17..d16bbcd7c 100644 --- a/internal/db/statusbookmark.go +++ b/internal/db/statusbookmark.go @@ -25,11 +25,16 @@ import ( type StatusBookmark interface { // GetStatusBookmark gets one status bookmark with the given ID. - GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, error) + GetStatusBookmarkByID(ctx context.Context, id string) (*gtsmodel.StatusBookmark, error) - // GetStatusBookmarkID is a shortcut function for returning just the database ID - // of a status bookmark created by the given accountID, targeting the given statusID. - GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, error) + // GetStatusBookmark fetches a status bookmark by the given account ID on the given status ID, if it exists. + GetStatusBookmark(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusBookmark, error) + + // IsStatusBookmarked returns whether status has been bookmarked by any account. + IsStatusBookmarked(ctx context.Context, statusID string) (bool, error) + + // IsStatusBookmarkedBy returns whether status ID is bookmarked by the given account ID. + IsStatusBookmarkedBy(ctx context.Context, accountID string, statusID string) (bool, error) // GetStatusBookmarks retrieves status bookmarks created by the given accountID, // and using the provided parameters. If limit is < 0 then no limit will be set. @@ -42,7 +47,7 @@ type StatusBookmark interface { PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) error // DeleteStatusBookmark deletes one status bookmark with the given ID. - DeleteStatusBookmark(ctx context.Context, id string) error + DeleteStatusBookmarkByID(ctx context.Context, id string) error // DeleteStatusBookmarks mass deletes status bookmarks targeting targetAccountID // and/or originating from originAccountID and/or bookmarking statusID. |