diff options
Diffstat (limited to 'internal/db')
-rw-r--r-- | internal/db/bundb/account.go | 31 | ||||
-rw-r--r-- | internal/db/bundb/bundb_test.go | 2 | ||||
-rw-r--r-- | internal/db/bundb/emoji.go | 51 | ||||
-rw-r--r-- | internal/db/bundb/errors.go | 3 | ||||
-rw-r--r-- | internal/db/bundb/media.go | 29 | ||||
-rw-r--r-- | internal/db/bundb/mention.go | 29 | ||||
-rw-r--r-- | internal/db/bundb/migrations/20230511181430_add_status_fetched_at.go | 47 | ||||
-rw-r--r-- | internal/db/bundb/notification.go | 107 | ||||
-rw-r--r-- | internal/db/bundb/relationship_block.go | 90 | ||||
-rw-r--r-- | internal/db/bundb/relationship_follow.go | 97 | ||||
-rw-r--r-- | internal/db/bundb/relationship_follow_req.go | 136 | ||||
-rw-r--r-- | internal/db/bundb/report.go | 28 | ||||
-rw-r--r-- | internal/db/bundb/status.go | 170 | ||||
-rw-r--r-- | internal/db/bundb/statusfave.go | 86 | ||||
-rw-r--r-- | internal/db/bundb/tombstone.go | 16 | ||||
-rw-r--r-- | internal/db/bundb/user.go | 50 |
16 files changed, 608 insertions, 364 deletions
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 56d46a232..f7e243f47 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -302,7 +302,7 @@ func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account columns = append(columns, "updated_at") } - err := a.state.Caches.GTS.Account().Store(account, func() error { + return a.state.Caches.GTS.Account().Store(account, func() error { // It is safe to run this database transaction within cache.Store // as the cache does not attempt a mutex lock until AFTER hook. // @@ -338,15 +338,23 @@ func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account return err }) }) - if err != nil { - return err - } - - return nil } func (a *accountDB) DeleteAccount(ctx context.Context, id string) db.Error { - if err := a.conn.RunInTx(ctx, func(tx bun.Tx) error { + defer a.state.Caches.GTS.Account().Invalidate("ID", id) + + // Load account into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := a.GetAccountByID(gtscontext.SetBarebones(ctx), id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // NOTE: even if db.ErrNoEntries is returned, we + // still run the below transaction to ensure related + // objects are appropriately deleted. + return err + } + + return a.conn.RunInTx(ctx, func(tx bun.Tx) error { // clear out any emoji links if _, err := tx. NewDelete(). @@ -363,14 +371,7 @@ func (a *accountDB) DeleteAccount(ctx context.Context, id string) db.Error { Where("? = ?", bun.Ident("account.id"), id). Exec(ctx) return err - }); err != nil { - return err - } - - // Invalidate account from database lookups. - a.state.Caches.GTS.Account().Invalidate("ID", id) - - return nil + }) } func (a *accountDB) GetAccountLastPosted(ctx context.Context, accountID string, webOnly bool) (time.Time, db.Error) { diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go index e6d482ac1..2566be2ba 100644 --- a/internal/db/bundb/bundb_test.go +++ b/internal/db/bundb/bundb_test.go @@ -66,9 +66,9 @@ func (suite *BunDBStandardTestSuite) SetupSuite() { } func (suite *BunDBStandardTestSuite) SetupTest() { - suite.state.Caches.Init() testrig.InitTestConfig() testrig.InitTestLog() + suite.state.Caches.Init() suite.db = testrig.NewTestDB(&suite.state) testrig.StandardDBSetup(suite.db, suite.testAccounts) } diff --git a/internal/db/bundb/emoji.go b/internal/db/bundb/emoji.go index 0c72be9d3..60b8fc12b 100644 --- a/internal/db/bundb/emoji.go +++ b/internal/db/bundb/emoji.go @@ -19,10 +19,12 @@ package bundb import ( "context" + "errors" "strings" "time" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -56,24 +58,46 @@ func (e *emojiDB) PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) db.Error } func (e *emojiDB) UpdateEmoji(ctx context.Context, emoji *gtsmodel.Emoji, columns ...string) (*gtsmodel.Emoji, db.Error) { - // Update the emoji's last-updated emoji.UpdatedAt = time.Now() + if len(columns) > 0 { + // If we're updating by column, ensure "updated_at" is included. + columns = append(columns, "updated_at") + } - if _, err := e.conn. - NewUpdate(). - Model(emoji). - Where("? = ?", bun.Ident("emoji.id"), emoji.ID). - Column(columns...). - Exec(ctx); err != nil { - return nil, e.conn.ProcessError(err) + err := e.state.Caches.GTS.Emoji().Store(emoji, func() error { + _, err := e.conn. + NewUpdate(). + Model(emoji). + Where("? = ?", bun.Ident("emoji.id"), emoji.ID). + Column(columns...). + Exec(ctx) + return e.conn.ProcessError(err) + }) + if err != nil { + return nil, err } - e.state.Caches.GTS.Emoji().Invalidate("ID", emoji.ID) return emoji, nil } func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) db.Error { - if err := e.conn.RunInTx(ctx, func(tx bun.Tx) error { + defer e.state.Caches.GTS.Emoji().Invalidate("ID", id) + + // Load emoji into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := e.GetEmojiByID( + gtscontext.SetBarebones(ctx), + id, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // NOTE: even if db.ErrNoEntries is returned, we + // still run the below transaction to ensure related + // objects are appropriately deleted. + return err + } + + return e.conn.RunInTx(ctx, func(tx bun.Tx) error { // delete links between this emoji and any statuses that use it if _, err := tx. NewDelete(). @@ -101,12 +125,7 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) db.Error { } return nil - }); err != nil { - return err - } - - e.state.Caches.GTS.Emoji().Invalidate("ID", id) - return nil + }) } func (e *emojiDB) GetEmojis(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, db.Error) { diff --git a/internal/db/bundb/errors.go b/internal/db/bundb/errors.go index 8de42b6aa..6236b82d8 100644 --- a/internal/db/bundb/errors.go +++ b/internal/db/bundb/errors.go @@ -52,7 +52,8 @@ func processSQLiteError(err error) db.Error { // Handle supplied error code: switch sqliteErr.Code() { - case sqlite3.SQLITE_CONSTRAINT_UNIQUE, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY: + case sqlite3.SQLITE_CONSTRAINT_UNIQUE, + sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY: return db.ErrAlreadyExists default: return err diff --git a/internal/db/bundb/media.go b/internal/db/bundb/media.go index d17d64b35..b64447beb 100644 --- a/internal/db/bundb/media.go +++ b/internal/db/bundb/media.go @@ -19,9 +19,11 @@ package bundb import ( "context" + "errors" "time" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -103,17 +105,26 @@ func (m *mediaDB) UpdateAttachment(ctx context.Context, media *gtsmodel.MediaAtt } func (m *mediaDB) DeleteAttachment(ctx context.Context, id string) error { - // Attempt to delete from database. - if _, err := m.conn.NewDelete(). - TableExpr("? AS ?", bun.Ident("media_attachments"), bun.Ident("media_attachment")). - Where("? = ?", bun.Ident("media_attachment.id"), id). - Exec(ctx); err != nil { - return m.conn.ProcessError(err) + defer m.state.Caches.GTS.Media().Invalidate("ID", id) + + // Load media into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := m.GetAttachmentByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err } - // Invalidate this media item from the cache. - m.state.Caches.GTS.Media().Invalidate("ID", id) - return nil + // Finally delete media from DB. + _, err = m.conn.NewDelete(). + TableExpr("? AS ?", bun.Ident("media_attachments"), bun.Ident("media_attachment")). + Where("? = ?", bun.Ident("media_attachment.id"), id). + Exec(ctx) + return m.conn.ProcessError(err) } func (m *mediaDB) GetRemoteOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.MediaAttachment, db.Error) { diff --git a/internal/db/bundb/mention.go b/internal/db/bundb/mention.go index e64d6dac4..9a41eb3b8 100644 --- a/internal/db/bundb/mention.go +++ b/internal/db/bundb/mention.go @@ -19,6 +19,7 @@ package bundb import ( "context" + "errors" "fmt" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -109,16 +110,24 @@ func (m *mentionDB) PutMention(ctx context.Context, mention *gtsmodel.Mention) e } func (m *mentionDB) DeleteMentionByID(ctx context.Context, id string) error { - if _, err := m.conn. - NewDelete(). - Table("mentions"). - Where("? = ?", bun.Ident("id"), id). - Exec(ctx); err != nil { - return m.conn.ProcessError(err) - } + defer m.state.Caches.GTS.Mention().Invalidate("ID", id) - // Invalidate mention from the lookup cache. - m.state.Caches.GTS.Mention().Invalidate("ID", id) + // Load mention into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := m.GetMention(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err + } - return nil + // Finally delete mention from DB. + _, err = m.conn.NewDelete(). + Table("mentions"). + Where("? = ?", bun.Ident("id"), id). + Exec(ctx) + return m.conn.ProcessError(err) } diff --git a/internal/db/bundb/migrations/20230511181430_add_status_fetched_at.go b/internal/db/bundb/migrations/20230511181430_add_status_fetched_at.go new file mode 100644 index 000000000..cc41b518e --- /dev/null +++ b/internal/db/bundb/migrations/20230511181430_add_status_fetched_at.go @@ -0,0 +1,47 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package migrations + +import ( + "context" + "strings" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + _, err := tx.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TIMESTAMPTZ", bun.Ident("statuses"), bun.Ident("fetched_at")) + if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) { + return err + } + return nil + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index f2ff60b9a..277a935fd 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -22,6 +22,7 @@ import ( "errors" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -179,16 +180,26 @@ func (n *notificationDB) PutNotification(ctx context.Context, notif *gtsmodel.No } func (n *notificationDB) DeleteNotificationByID(ctx context.Context, id string) db.Error { - if _, err := n.conn. - NewDelete(). - TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")). - Where("? = ?", bun.Ident("notification.id"), id). - Exec(ctx); err != nil { - return n.conn.ProcessError(err) + defer n.state.Caches.GTS.Notification().Invalidate("ID", id) + + // Load notif into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := n.GetNotificationByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err } - n.state.Caches.GTS.Notification().Invalidate("ID", id) - return nil + // Finally delete notif from DB. + _, err = n.conn.NewDelete(). + TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")). + Where("? = ?", bun.Ident("notification.id"), id). + Exec(ctx) + return n.conn.ProcessError(err) } func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) db.Error { @@ -196,56 +207,88 @@ func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string return errors.New("DeleteNotifications: one of targetAccountID or originAccountID must be set") } - // Capture notification IDs in a RETURNING statement. - var ids []string + var notifIDs []string q := n.conn. - NewDelete(). - TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")). - Returning("?", bun.Ident("id")) + NewSelect(). + Column("id"). + Table("notifications") if len(types) > 0 { - q = q.Where("? IN (?)", bun.Ident("notification.notification_type"), bun.In(types)) + q = q.Where("? IN (?)", bun.Ident("notification_type"), bun.In(types)) } if targetAccountID != "" { - q = q.Where("? = ?", bun.Ident("notification.target_account_id"), targetAccountID) + q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID) } if originAccountID != "" { - q = q.Where("? = ?", bun.Ident("notification.origin_account_id"), originAccountID) + q = q.Where("? = ?", bun.Ident("origin_account_id"), originAccountID) } - if _, err := q.Exec(ctx, &ids); err != nil { + if _, err := q.Exec(ctx, ¬ifIDs); err != nil { return n.conn.ProcessError(err) } - // Invalidate each returned ID. - for _, id := range ids { - n.state.Caches.GTS.Notification().Invalidate("ID", id) + defer func() { + // Invalidate all IDs on return. + for _, id := range notifIDs { + n.state.Caches.GTS.Notification().Invalidate("ID", id) + } + }() + + // Load all notif into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). + for _, id := range notifIDs { + _, err := n.GetNotificationByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := n.conn.NewDelete(). + Table("notifications"). + Where("? IN (?)", bun.Ident("id"), bun.In(notifIDs)). + Exec(ctx) + return n.conn.ProcessError(err) } func (n *notificationDB) DeleteNotificationsForStatus(ctx context.Context, statusID string) db.Error { - // Capture notification IDs in a RETURNING statement. - var ids []string + var notifIDs []string q := n.conn. - NewDelete(). - TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")). - Where("? = ?", bun.Ident("notification.status_id"), statusID). - Returning("?", bun.Ident("id")) + NewSelect(). + Column("id"). + Table("notifications"). + Where("? = ?", bun.Ident("status_id"), statusID) - if _, err := q.Exec(ctx, &ids); err != nil { + if _, err := q.Exec(ctx, ¬ifIDs); err != nil { return n.conn.ProcessError(err) } - // Invalidate each returned ID. - for _, id := range ids { - n.state.Caches.GTS.Notification().Invalidate("ID", id) + defer func() { + // Invalidate all IDs on return. + for _, id := range notifIDs { + n.state.Caches.GTS.Notification().Invalidate("ID", id) + } + }() + + // Load all notif into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). + for _, id := range notifIDs { + _, err := n.GetNotificationByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := n.conn.NewDelete(). + Table("notifications"). + Where("? IN (?)", bun.Ident("id"), bun.In(notifIDs)). + Exec(ctx) + return n.conn.ProcessError(err) } diff --git a/internal/db/bundb/relationship_block.go b/internal/db/bundb/relationship_block.go index 9232ea984..fa68a2e97 100644 --- a/internal/db/bundb/relationship_block.go +++ b/internal/db/bundb/relationship_block.go @@ -25,7 +25,6 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/uptrace/bun" ) @@ -142,62 +141,65 @@ func (r *relationshipDB) getBlock(ctx context.Context, lookup string, dbQuery fu } func (r *relationshipDB) PutBlock(ctx context.Context, block *gtsmodel.Block) error { - err := r.state.Caches.GTS.Block().Store(block, func() error { + return r.state.Caches.GTS.Block().Store(block, func() error { _, err := r.conn.NewInsert().Model(block).Exec(ctx) return r.conn.ProcessError(err) }) - if err != nil { - return err - } - - // Invalidate block origin account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", block.AccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", block.AccountID) - - // Invalidate block target account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", block.TargetAccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", block.TargetAccountID) - - return nil } func (r *relationshipDB) DeleteBlockByID(ctx context.Context, id string) error { - block, err := r.GetBlockByID(gtscontext.SetBarebones(ctx), id) + defer r.state.Caches.GTS.Block().Invalidate("ID", id) + + // Load block into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetBlockByID(gtscontext.SetBarebones(ctx), id) if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } return err } - return r.deleteBlock(ctx, block) + + // Finally delete block from DB. + _, err = r.conn.NewDelete(). + Table("blocks"). + Where("? = ?", bun.Ident("id"), id). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteBlockByURI(ctx context.Context, uri string) error { - block, err := r.GetBlockByURI(gtscontext.SetBarebones(ctx), uri) + defer r.state.Caches.GTS.Block().Invalidate("URI", uri) + + // Load block into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetBlockByURI(gtscontext.SetBarebones(ctx), uri) if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } return err } - return r.deleteBlock(ctx, block) -} -func (r *relationshipDB) deleteBlock(ctx context.Context, block *gtsmodel.Block) error { - if _, err := r.conn. - NewDelete(). + // Finally delete block from DB. + _, err = r.conn.NewDelete(). Table("blocks"). - Where("? = ?", bun.Ident("id"), block.ID). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) - } - - // Invalidate block from cache lookups. - r.state.Caches.GTS.Block().Invalidate("ID", block.ID) - - return nil + Where("? = ?", bun.Ident("uri"), uri). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteAccountBlocks(ctx context.Context, accountID string) error { var blockIDs []string + // Get full list of IDs. if err := r.conn.NewSelect(). + Column("id"). Table("blocks"). - ColumnExpr("?", bun.Ident("id")). WhereOr("? = ? OR ? = ?", bun.Ident("account_id"), accountID, @@ -208,11 +210,27 @@ func (r *relationshipDB) DeleteAccountBlocks(ctx context.Context, accountID stri return r.conn.ProcessError(err) } + defer func() { + // Invalidate all IDs on return. + for _, id := range blockIDs { + r.state.Caches.GTS.Block().Invalidate("ID", id) + } + }() + + // Load all blocks into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). for _, id := range blockIDs { - if err := r.DeleteBlockByID(ctx, id); err != nil { - log.Errorf(ctx, "error deleting block %q: %v", id, err) + _, err := r.GetBlockByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err } } - return nil + // Finally delete all from DB. + _, err := r.conn.NewDelete(). + Table("blocks"). + Where("? IN (?)", bun.Ident("id"), bun.In(blockIDs)). + Exec(ctx) + return r.conn.ProcessError(err) } diff --git a/internal/db/bundb/relationship_follow.go b/internal/db/bundb/relationship_follow.go index 1b1de77b1..fe1f26bf1 100644 --- a/internal/db/bundb/relationship_follow.go +++ b/internal/db/bundb/relationship_follow.go @@ -171,23 +171,10 @@ func (r *relationshipDB) getFollow(ctx context.Context, lookup string, dbQuery f } func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error { - err := r.state.Caches.GTS.Follow().Store(follow, func() error { + return r.state.Caches.GTS.Follow().Store(follow, func() error { _, err := r.conn.NewInsert().Model(follow).Exec(ctx) return r.conn.ProcessError(err) }) - if err != nil { - return err - } - - // Invalidate follow origin account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", follow.AccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", follow.AccountID) - - // Invalidate follow target account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", follow.TargetAccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", follow.TargetAccountID) - - return nil } func (r *relationshipDB) UpdateFollow(ctx context.Context, follow *gtsmodel.Follow, columns ...string) error { @@ -211,38 +198,58 @@ func (r *relationshipDB) UpdateFollow(ctx context.Context, follow *gtsmodel.Foll } func (r *relationshipDB) DeleteFollowByID(ctx context.Context, id string) error { - if _, err := r.conn.NewDelete(). - Table("follows"). - Where("? = ?", bun.Ident("id"), id). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) - } + defer r.state.Caches.GTS.Follow().Invalidate("ID", id) - // Invalidate follow from cache lookups. - r.state.Caches.GTS.Follow().Invalidate("ID", id) + // Load follow into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetFollowByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err + } - return nil + // Finally delete follow from DB. + _, err = r.conn.NewDelete(). + Table("follows"). + Where("? = ?", bun.Ident("id"), id). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteFollowByURI(ctx context.Context, uri string) error { - if _, err := r.conn.NewDelete(). - Table("follows"). - Where("? = ?", bun.Ident("uri"), uri). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) - } + defer r.state.Caches.GTS.Follow().Invalidate("URI", uri) - // Invalidate follow from cache lookups. - r.state.Caches.GTS.Follow().Invalidate("URI", uri) + // Load follow into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetFollowByURI(gtscontext.SetBarebones(ctx), uri) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err + } - return nil + // Finally delete follow from DB. + _, err = r.conn.NewDelete(). + Table("follows"). + Where("? = ?", bun.Ident("uri"), uri). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID string) error { var followIDs []string + // Get full list of IDs. if _, err := r.conn. - NewDelete(). + NewSelect(). + Column("id"). Table("follows"). WhereOr("? = ? OR ? = ?", bun.Ident("account_id"), @@ -250,15 +257,31 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str bun.Ident("target_account_id"), accountID, ). - Returning("?", bun.Ident("id")). Exec(ctx, &followIDs); err != nil { return r.conn.ProcessError(err) } - // Invalidate each returned ID. + defer func() { + // Invalidate all IDs on return. + for _, id := range followIDs { + r.state.Caches.GTS.Follow().Invalidate("ID", id) + } + }() + + // Load all follows into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). for _, id := range followIDs { - r.state.Caches.GTS.Follow().Invalidate("ID", id) + _, err := r.GetFollowByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := r.conn.NewDelete(). + Table("follows"). + Where("? IN (?)", bun.Ident("id"), bun.In(followIDs)). + Exec(ctx) + return r.conn.ProcessError(err) } diff --git a/internal/db/bundb/relationship_follow_req.go b/internal/db/bundb/relationship_follow_req.go index c0f4bce3b..c2ac3d2f1 100644 --- a/internal/db/bundb/relationship_follow_req.go +++ b/internal/db/bundb/relationship_follow_req.go @@ -149,23 +149,10 @@ func (r *relationshipDB) getFollowRequest(ctx context.Context, lookup string, db } func (r *relationshipDB) PutFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error { - err := r.state.Caches.GTS.FollowRequest().Store(follow, func() error { + return r.state.Caches.GTS.FollowRequest().Store(follow, func() error { _, err := r.conn.NewInsert().Model(follow).Exec(ctx) return r.conn.ProcessError(err) }) - if err != nil { - return err - } - - // Invalidate follow request origin account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", follow.AccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", follow.AccountID) - - // Invalidate follow request target account ID cached visibility. - r.state.Caches.Visibility.Invalidate("ItemID", follow.TargetAccountID) - r.state.Caches.Visibility.Invalidate("RequesterID", follow.TargetAccountID) - - return nil } func (r *relationshipDB) UpdateFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest, columns ...string) error { @@ -221,6 +208,9 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI return nil, err } + // Invalidate follow request from cache lookups on return. + defer r.state.Caches.GTS.FollowRequest().Invalidate("ID", followReq.ID) + // Delete original follow request. if _, err := r.conn. NewDelete(). @@ -230,9 +220,6 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI return nil, r.conn.ProcessError(err) } - // Invalidate follow request from cache lookups - r.state.Caches.GTS.FollowRequest().Invalidate("ID", followReq.ID) - // Delete original follow request notification if err := r.state.DB.DeleteNotifications(ctx, []string{ string(gtsmodel.NotificationFollowRequest), @@ -244,15 +231,30 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI } func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountID string, targetAccountID string) db.Error { - // Get original follow request. - followReq, err := r.GetFollowRequest(ctx, sourceAccountID, targetAccountID) + defer r.state.Caches.GTS.FollowRequest().Invalidate("AccountID.TargetAccountID", sourceAccountID, targetAccountID) + + // Load followreq into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetFollowRequest(gtscontext.SetBarebones(ctx), + sourceAccountID, + targetAccountID, + ) if err != nil { return err } - // Delete original follow request. - if err := r.DeleteFollowRequestByID(ctx, followReq.ID); err != nil { - return err + // Attempt to delete follow request. + if _, err = r.conn.NewDelete(). + Table("follow_requests"). + Where("? = ? AND ? = ?", + bun.Ident("account_id"), + sourceAccountID, + bun.Ident("target_account_id"), + targetAccountID, + ). + Exec(ctx); err != nil { + return r.conn.ProcessError(err) } // Delete original follow request notification @@ -262,54 +264,90 @@ func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountI } func (r *relationshipDB) DeleteFollowRequestByID(ctx context.Context, id string) error { - if _, err := r.conn.NewDelete(). - Table("follow_requests"). - Where("? = ?", bun.Ident("id"), id). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) - } + defer r.state.Caches.GTS.FollowRequest().Invalidate("ID", id) - // Invalidate follow request from cache lookups. - r.state.Caches.GTS.FollowRequest().Invalidate("ID", id) + // Load followreq into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetFollowRequestByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err + } - return nil + // Finally delete followreq from DB. + _, err = r.conn.NewDelete(). + Table("follow_requests"). + Where("? = ?", bun.Ident("id"), id). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteFollowRequestByURI(ctx context.Context, uri string) error { - if _, err := r.conn.NewDelete(). - Table("follow_requests"). - Where("? = ?", bun.Ident("uri"), uri). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) - } + defer r.state.Caches.GTS.FollowRequest().Invalidate("URI", uri) - // Invalidate follow request from cache lookups. - r.state.Caches.GTS.FollowRequest().Invalidate("URI", uri) + // Load followreq into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetFollowRequestByURI(gtscontext.SetBarebones(ctx), uri) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err + } - return nil + // Finally delete followreq from DB. + _, err = r.conn.NewDelete(). + Table("follow_requests"). + Where("? = ?", bun.Ident("uri"), uri). + Exec(ctx) + return r.conn.ProcessError(err) } func (r *relationshipDB) DeleteAccountFollowRequests(ctx context.Context, accountID string) error { - var followIDs []string + var followReqIDs []string + // Get full list of IDs. if _, err := r.conn. - NewDelete(). - Table("follow_requests"). + NewSelect(). + Column("id"). + Table("follow_requestss"). WhereOr("? = ? OR ? = ?", bun.Ident("account_id"), accountID, bun.Ident("target_account_id"), accountID, ). - Returning("?", bun.Ident("id")). - Exec(ctx, &followIDs); err != nil { + Exec(ctx, &followReqIDs); err != nil { return r.conn.ProcessError(err) } - // Invalidate each returned ID. - for _, id := range followIDs { - r.state.Caches.GTS.FollowRequest().Invalidate("ID", id) + defer func() { + // Invalidate all IDs on return. + for _, id := range followReqIDs { + r.state.Caches.GTS.FollowRequest().Invalidate("ID", id) + } + }() + + // Load all followreqs into cache, this *really* isn't + // great but it is the only way we can ensure we invalidate + // all related caches correctly (e.g. visibility). + for _, id := range followReqIDs { + _, err := r.GetFollowRequestByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := r.conn.NewDelete(). + Table("follow_requests"). + Where("? IN (?)", bun.Ident("id"), bun.In(followReqIDs)). + Exec(ctx) + return r.conn.ProcessError(err) } diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go index 17e1348b9..e017a8906 100644 --- a/internal/db/bundb/report.go +++ b/internal/db/bundb/report.go @@ -19,10 +19,12 @@ package bundb import ( "context" + "errors" "fmt" "time" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -192,14 +194,24 @@ func (r *reportDB) UpdateReport(ctx context.Context, report *gtsmodel.Report, co } func (r *reportDB) DeleteReportByID(ctx context.Context, id string) db.Error { - if _, err := r.conn. - NewDelete(). - TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")). - Where("? = ?", bun.Ident("report.id"), id). - Exec(ctx); err != nil { - return r.conn.ProcessError(err) + defer r.state.Caches.GTS.Report().Invalidate("ID", id) + + // Load status into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := r.GetReportByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err } - r.state.Caches.GTS.Report().Invalidate("ID", id) - return nil + // Finally delete report from DB. + _, err = r.conn.NewDelete(). + TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")). + Where("? = ?", bun.Ident("report.id"), id). + Exec(ctx) + return r.conn.ProcessError(err) } diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 49d4f938b..0dffbabcc 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -244,7 +244,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) } func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Error { - err := s.state.Caches.GTS.Status().Store(status, func() error { + return s.state.Caches.GTS.Status().Store(status, func() error { // It is safe to run this database transaction within cache.Store // as the cache does not attempt a mutex lock until AFTER hook. // @@ -304,21 +304,6 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Er return err }) }) - if err != nil { - return err - } - - for _, id := range status.AttachmentIDs { - // Invalidate media attachments from cache. - // - // NOTE: this is needed due to the way in which - // we upload status attachments, and only after - // update them with a known status ID. This is - // not the case for header/avatar attachments. - s.state.Caches.GTS.Media().Invalidate("ID", id) - } - - return nil } func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) db.Error { @@ -328,88 +313,91 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co columns = append(columns, "updated_at") } - if err := s.conn.RunInTx(ctx, func(tx bun.Tx) error { - // create links between this status and any emojis it uses - for _, i := range status.EmojiIDs { - if _, err := tx. - NewInsert(). - Model(>smodel.StatusToEmoji{ - StatusID: status.ID, - EmojiID: i, - }). - On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")). - Exec(ctx); err != nil { - err = s.conn.ProcessError(err) - if !errors.Is(err, db.ErrAlreadyExists) { - return err + return s.state.Caches.GTS.Status().Store(status, func() error { + // It is safe to run this database transaction within cache.Store + // as the cache does not attempt a mutex lock until AFTER hook. + // + return s.conn.RunInTx(ctx, func(tx bun.Tx) error { + // create links between this status and any emojis it uses + for _, i := range status.EmojiIDs { + if _, err := tx. + NewInsert(). + Model(>smodel.StatusToEmoji{ + StatusID: status.ID, + EmojiID: i, + }). + On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")). + Exec(ctx); err != nil { + err = s.conn.ProcessError(err) + if !errors.Is(err, db.ErrAlreadyExists) { + return err + } } } - } - // create links between this status and any tags it uses - for _, i := range status.TagIDs { - if _, err := tx. - NewInsert(). - Model(>smodel.StatusToTag{ - StatusID: status.ID, - TagID: i, - }). - On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")). - Exec(ctx); err != nil { - err = s.conn.ProcessError(err) - if !errors.Is(err, db.ErrAlreadyExists) { - return err + // create links between this status and any tags it uses + for _, i := range status.TagIDs { + if _, err := tx. + NewInsert(). + Model(>smodel.StatusToTag{ + StatusID: status.ID, + TagID: i, + }). + On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")). + Exec(ctx); err != nil { + err = s.conn.ProcessError(err) + if !errors.Is(err, db.ErrAlreadyExists) { + return err + } } } - } - // change the status ID of the media attachments to the new status - for _, a := range status.Attachments { - a.StatusID = status.ID - a.UpdatedAt = time.Now() - if _, err := tx. - NewUpdate(). - Model(a). - Where("? = ?", bun.Ident("media_attachment.id"), a.ID). - Exec(ctx); err != nil { - err = s.conn.ProcessError(err) - if !errors.Is(err, db.ErrAlreadyExists) { - return err + // change the status ID of the media attachments to the new status + for _, a := range status.Attachments { + a.StatusID = status.ID + a.UpdatedAt = time.Now() + if _, err := tx. + NewUpdate(). + Model(a). + Where("? = ?", bun.Ident("media_attachment.id"), a.ID). + Exec(ctx); err != nil { + err = s.conn.ProcessError(err) + if !errors.Is(err, db.ErrAlreadyExists) { + return err + } } } - } - // Finally, update the status - _, err := tx. - NewUpdate(). - Model(status). - Column(columns...). - Where("? = ?", bun.Ident("status.id"), status.ID). - Exec(ctx) - return err - }); err != nil { - // already processed - return err - } + // Finally, update the status + _, err := tx. + NewUpdate(). + Model(status). + Column(columns...). + Where("? = ?", bun.Ident("status.id"), status.ID). + Exec(ctx) + return err + }) + }) +} - // Invalidate status from database lookups. - s.state.Caches.GTS.Status().Invalidate("ID", status.ID) +func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error { + defer s.state.Caches.GTS.Status().Invalidate("ID", id) - for _, id := range status.AttachmentIDs { - // Invalidate media attachments from cache. - // - // NOTE: this is needed due to the way in which - // we upload status attachments, and only after - // update them with a known status ID. This is - // not the case for header/avatar attachments. - s.state.Caches.GTS.Media().Invalidate("ID", id) + // Load status into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := s.GetStatusByID( + gtscontext.SetBarebones(ctx), + id, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // NOTE: even if db.ErrNoEntries is returned, we + // still run the below transaction to ensure related + // objects are appropriately deleted. + return err } - return nil -} - -func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error { - if err := s.conn.RunInTx(ctx, func(tx bun.Tx) error { + return s.conn.RunInTx(ctx, func(tx bun.Tx) error { // delete links between this status and any emojis it uses if _, err := tx. NewDelete(). @@ -438,17 +426,7 @@ func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error { } return nil - }); err != nil { - return err - } - - // Invalidate status from database lookups. - s.state.Caches.GTS.Status().Invalidate("ID", id) - - // Invalidate status from all visibility lookups. - s.state.Caches.Visibility.Invalidate("ItemID", id) - - return nil + }) } func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { diff --git a/internal/db/bundb/statusfave.go b/internal/db/bundb/statusfave.go index 0f7e5df74..497262530 100644 --- a/internal/db/bundb/statusfave.go +++ b/internal/db/bundb/statusfave.go @@ -156,16 +156,26 @@ func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusF } func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) db.Error { - if _, err := s.conn. - NewDelete(). - Table("status_faves"). - Where("? = ?", bun.Ident("id"), id). - Exec(ctx); err != nil { - return s.conn.ProcessError(err) + defer s.state.Caches.GTS.StatusFave().Invalidate("ID", id) + + // Load fave into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := s.GetStatusFaveByID(gtscontext.SetBarebones(ctx), id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err } - s.state.Caches.GTS.StatusFave().Invalidate("ID", id) - return nil + // Finally delete fave from DB. + _, err = s.conn.NewDelete(). + Table("status_faves"). + Where("? = ?", bun.Ident("id"), id). + Exec(ctx) + return s.conn.ProcessError(err) } func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) db.Error { @@ -173,13 +183,12 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set") } - // Capture fave IDs in a RETURNING statement. var faveIDs []string q := s.conn. - NewDelete(). - Table("status_faves"). - Returning("?", bun.Ident("id")) + NewSelect(). + Column("id"). + Table("status_faves") if targetAccountID != "" { q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID) @@ -193,12 +202,29 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st return s.conn.ProcessError(err) } + defer func() { + // Invalidate all IDs on return. + for _, id := range faveIDs { + s.state.Caches.GTS.StatusFave().Invalidate("ID", id) + } + }() + + // Load all faves into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). for _, id := range faveIDs { - // Invalidate each of the returned status fave IDs. - s.state.Caches.GTS.StatusFave().Invalidate("ID", id) + _, err := s.GetStatusFaveByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := s.conn.NewDelete(). + Table("status_faves"). + Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)). + Exec(ctx) + return s.conn.ProcessError(err) } func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) db.Error { @@ -206,19 +232,35 @@ func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID var faveIDs []string q := s.conn. - NewDelete(). + NewSelect(). + Column("id"). Table("status_faves"). - Where("? = ?", bun.Ident("status_id"), statusID). - Returning("?", bun.Ident("id")) - + Where("? = ?", bun.Ident("status_id"), statusID) if _, err := q.Exec(ctx, &faveIDs); err != nil { return s.conn.ProcessError(err) } + defer func() { + // Invalidate all IDs on return. + for _, id := range faveIDs { + s.state.Caches.GTS.StatusFave().Invalidate("ID", id) + } + }() + + // Load all faves into cache, this *really* isn't great + // but it is the only way we can ensure we invalidate all + // related caches correctly (e.g. visibility). for _, id := range faveIDs { - // Invalidate each of the returned status fave IDs. - s.state.Caches.GTS.StatusFave().Invalidate("ID", id) + _, err := s.GetStatusFaveByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } } - return nil + // Finally delete all from DB. + _, err := s.conn.NewDelete(). + Table("status_faves"). + Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)). + Exec(ctx) + return s.conn.ProcessError(err) } diff --git a/internal/db/bundb/tombstone.go b/internal/db/bundb/tombstone.go index 393b2e356..668bde1af 100644 --- a/internal/db/bundb/tombstone.go +++ b/internal/db/bundb/tombstone.go @@ -67,16 +67,12 @@ func (t *tombstoneDB) PutTombstone(ctx context.Context, tombstone *gtsmodel.Tomb } func (t *tombstoneDB) DeleteTombstone(ctx context.Context, id string) db.Error { - if _, err := t.conn. - NewDelete(). + defer t.state.Caches.GTS.Tombstone().Invalidate("ID", id) + + // Delete tombstone from DB. + _, err := t.conn.NewDelete(). TableExpr("? AS ?", bun.Ident("tombstones"), bun.Ident("tombstone")). Where("? = ?", bun.Ident("tombstone.id"), id). - Exec(ctx); err != nil { - return t.conn.ProcessError(err) - } - - // Invalidate from cache by ID - t.state.Caches.GTS.Tombstone().Invalidate("ID", id) - - return nil + Exec(ctx) + return t.conn.ProcessError(err) } diff --git a/internal/db/bundb/user.go b/internal/db/bundb/user.go index b5dae1573..c2ea5a67d 100644 --- a/internal/db/bundb/user.go +++ b/internal/db/bundb/user.go @@ -19,9 +19,11 @@ package bundb import ( "context" + "errors" "time" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/uptrace/bun" @@ -155,32 +157,36 @@ func (u *userDB) UpdateUser(ctx context.Context, user *gtsmodel.User, columns .. columns = append(columns, "updated_at") } - // Update the user in DB - _, err := u.conn. - NewUpdate(). - Model(user). - Where("? = ?", bun.Ident("user.id"), user.ID). - Column(columns...). - Exec(ctx) - if err != nil { + return u.state.Caches.GTS.User().Store(user, func() error { + _, err := u.conn. + NewUpdate(). + Model(user). + Where("? = ?", bun.Ident("user.id"), user.ID). + Column(columns...). + Exec(ctx) return u.conn.ProcessError(err) - } - - // Invalidate user from cache - u.state.Caches.GTS.User().Invalidate("ID", user.ID) - return nil + }) } func (u *userDB) DeleteUserByID(ctx context.Context, userID string) db.Error { - if _, err := u.conn. - NewDelete(). - TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). - Where("? = ?", bun.Ident("user.id"), userID). - Exec(ctx); err != nil { - return u.conn.ProcessError(err) + defer u.state.Caches.GTS.User().Invalidate("ID", userID) + + // Load user into cache before attempting a delete, + // as we need it cached in order to trigger the invalidate + // callback. This in turn invalidates others. + _, err := u.GetUserByID(gtscontext.SetBarebones(ctx), userID) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // not an issue. + err = nil + } + return err } - // Invalidate user from cache - u.state.Caches.GTS.User().Invalidate("ID", userID) - return nil + // Finally delete user from DB. + _, err = u.conn.NewDelete(). + TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). + Where("? = ?", bun.Ident("user.id"), userID). + Exec(ctx) + return u.conn.ProcessError(err) } |