// 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 bundb import ( "context" "errors" "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/xslices" "github.com/uptrace/bun" ) type statusEditDB struct { db *bun.DB state *state.State } func (s *statusEditDB) GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error) { // Fetch edit from database cache with loader callback. edit, err := s.state.Caches.DB.StatusEdit.LoadOne("ID", func() (*gtsmodel.StatusEdit, error) { var edit gtsmodel.StatusEdit // Not cached, load edit // from database by its ID. if err := s.db.NewSelect(). Model(&edit). Where("? = ?", bun.Ident("id"), id). Scan(ctx); err != nil { return nil, err } return &edit, nil }, id, ) if err != nil { return nil, err } if gtscontext.Barebones(ctx) { // no need to fully populate. return edit, nil } // Further populate the edit fields where applicable. if err := s.PopulateStatusEdit(ctx, edit); err != nil { return nil, err } return edit, nil } func (s *statusEditDB) GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error) { // Load status edits for IDs via cache loader callbacks. edits, err := s.state.Caches.DB.StatusEdit.LoadIDs("ID", ids, func(uncached []string) ([]*gtsmodel.StatusEdit, error) { // Preallocate expected length of uncached edits. edits := make([]*gtsmodel.StatusEdit, 0, len(uncached)) // Perform database query scanning // the remaining (uncached) edit IDs. if err := s.db.NewSelect(). Model(&edits). Where("? IN (?)", bun.Ident("id"), bun.In(uncached)). Scan(ctx); err != nil { return nil, err } return edits, nil }, ) if err != nil { return nil, err } // Reorder the edits by their // IDs to ensure in correct order. getID := func(e *gtsmodel.StatusEdit) string { return e.ID } xslices.OrderBy(edits, ids, getID) if gtscontext.Barebones(ctx) { // no need to fully populate. return edits, nil } // Populate all loaded edits, removing those we fail to // populate (removes needing so many nil checks everywhere). edits = slices.DeleteFunc(edits, func(edit *gtsmodel.StatusEdit) bool { if err := s.PopulateStatusEdit(ctx, edit); err != nil { log.Errorf(ctx, "error populating edit %s: %v", edit.ID, err) return true } return false }) return edits, nil } func (s *statusEditDB) PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error { var err error var errs gtserror.MultiError // For sub-models we only want // barebones versions of them. ctx = gtscontext.SetBarebones(ctx) if !edit.AttachmentsPopulated() { // Fetch all attachments for status edit's IDs. edit.Attachments, err = s.state.DB.GetAttachmentsByIDs( ctx, edit.AttachmentIDs, ) if err != nil { errs.Appendf("error populating edit attachments: %w", err) } } return errs.Combine() } func (s *statusEditDB) PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error { return s.state.Caches.DB.StatusEdit.Store(edit, func() error { _, err := s.db.NewInsert().Model(edit).Exec(ctx) return err }) } func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) error { // Gather necessary fields from // deleted for cache invalidation. deleted := make([]*gtsmodel.StatusEdit, 0, len(ids)) // Delete all edits with IDs pertaining // to given slice, returning status IDs. if _, err := s.db.NewDelete(). Model(&deleted). Where("? IN (?)", bun.Ident("id"), bun.In(ids)). Returning("?", bun.Ident("status_id")). Exec(ctx); err != nil && !errors.Is(err, db.ErrNoEntries) { return err } // Check for no deletes. if len(deleted) == 0 { return nil } // Invalidate all the cached status edits with IDs. s.state.Caches.DB.StatusEdit.InvalidateIDs("ID", ids) // With each invalidate hook mark status ID of // edit we just called for. We only want to call // invalidate hooks of edits from unique statuses. invalidated := make(map[string]struct{}, 1) // Invalidate the first delete manually, this // opt negates need for initial hashmap lookup. s.state.Caches.OnInvalidateStatusEdit(deleted[0]) invalidated[deleted[0].StatusID] = struct{}{} for _, edit := range deleted { // Check not already called for status. _, ok := invalidated[edit.StatusID] if ok { continue } // Manually call status edit invalidate hook. s.state.Caches.OnInvalidateStatusEdit(edit) invalidated[edit.StatusID] = struct{}{} } return nil }