mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Remove QueryRowx (no-timeout) from sqlxDBWrapper; migrate caller to QueryRowX QueryRowx forwarded to context.Background() with no timeout, while QueryRowX (uppercase) already enforces the wrapper timeout. Removing the no-timeout variant eliminates an accidental footgun and migrates the one sqlstore caller (plugin_store) to the timeout-enforcing method. * sqlxRow, with timeout cancel after Scan Introduce sqlxRow, which pairs *sqlx.Row with its context cancel function. QueryRowX (on both sqlxDBWrapper and sqlxTxWrapper) now returns *sqlxRow; Scan calls cancel immediately after the row is consumed, releasing the timeout context as soon as possible rather than waiting for the timer. * sqlxRows, with timeout cancel on Close Introduce sqlxRows, which embeds *sqlx.Rows and holds the timeout context's cancel function. Close() cancels the context immediately after the rows are done, releasing timeout resources as soon as iteration completes rather than waiting for the timer to fire. Introduce rowScanner interface (Next/Scan/Err) so the internal helpers scanRowsIntoMap and scanRetentionIdsForDeletion accept any row iterator rather than the concrete *sql.Rows, accommodating the new return types without threading *sqlxRows through every caller. * abolish the X suffix altogether * delete unused NamedQuery * add timeout tests for Query and QueryRow on db and tx wrappers * fix tx/timeout tests: handle pq driver.ErrBadConn on killed connection * fixup! fix tx/timeout tests: handle pq driver.ErrBadConn on killed connection * override Next() on sqlxRows to cancel on EOF * rm redundant row.Err(), handled by Scan * apply timeout to context unless deadline set * rebind consistently * address review feedback: rename, trace, and QueryContext fix - Rename withQueryTimeout → ensureQueryTimeout to better convey that it respects existing deadlines rather than overriding them. - Add missing w.trace blocks to QueryRowContext and ExecContext. - Change QueryContext to use ensureQueryTimeout and return *sqlxRows (instead of *sql.Rows) so the cancel is deferred to Close/Next rather than released prematurely. * fix Beginx → Begin after master merge * fix golangci-lint inline warning in sqlx_wrapper_test --------- Co-authored-by: Mattermost Build <[email protected]>
277 lines
7.3 KiB
Go
277 lines
7.3 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sqlstore
|
|
|
|
import (
|
|
"database/sql"
|
|
"slices"
|
|
"strconv"
|
|
|
|
sq "github.com/mattermost/squirrel"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
)
|
|
|
|
type SqlViewStore struct {
|
|
*SqlStore
|
|
viewSelect sq.SelectBuilder
|
|
}
|
|
|
|
func newSqlViewStore(sqlStore *SqlStore) store.ViewStore {
|
|
s := &SqlViewStore{
|
|
SqlStore: sqlStore,
|
|
}
|
|
|
|
s.viewSelect = s.getQueryBuilder().
|
|
Select(viewColumns()...).
|
|
From("Views")
|
|
|
|
return s
|
|
}
|
|
|
|
func viewColumns() []string {
|
|
return []string{
|
|
"Id", "ChannelId", "Type", "CreatorId", "Title",
|
|
"Description", "SortOrder", "Props",
|
|
"CreateAt", "UpdateAt", "DeleteAt",
|
|
}
|
|
}
|
|
|
|
func (s *SqlViewStore) Save(view *model.View) (*model.View, error) {
|
|
if err := s.saveViewT(s.GetMaster(), view); err != nil {
|
|
return nil, err
|
|
}
|
|
return view, nil
|
|
}
|
|
|
|
// saveViewT inserts a view row using the provided executor (either a
|
|
// transaction or a regular master DB handle). The caller is responsible for
|
|
// setting view.ChannelId before calling. PreSave + IsValid are run here so
|
|
// callers can't forget to validate.
|
|
func (s *SqlViewStore) saveViewT(ex sqlxExecutor, view *model.View) error {
|
|
view.PreSave()
|
|
if err := view.IsValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
builder := s.getQueryBuilder().
|
|
Insert("Views").
|
|
Columns(viewColumns()...).
|
|
Values(
|
|
view.Id, view.ChannelId, view.Type, view.CreatorId, view.Title,
|
|
view.Description, view.SortOrder, view.Props,
|
|
view.CreateAt, view.UpdateAt, view.DeleteAt,
|
|
)
|
|
|
|
if _, err := ex.ExecBuilder(builder); err != nil {
|
|
return errors.Wrap(err, "failed to save view")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlViewStore) Get(id string) (*model.View, error) {
|
|
builder := s.viewSelect.Where(sq.Eq{"Id": id, "DeleteAt": 0})
|
|
|
|
var view model.View
|
|
if err := s.GetReplica().GetBuilder(&view, builder); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, store.NewErrNotFound("View", id)
|
|
}
|
|
return nil, errors.Wrapf(err, "failed to get view with id=%s", id)
|
|
}
|
|
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *SqlViewStore) GetForChannel(channelID string, opts model.ViewQueryOpts) ([]*model.View, error) {
|
|
if channelID == "" {
|
|
return nil, store.NewErrInvalidInput("View", "channelID", channelID)
|
|
}
|
|
|
|
if opts.PerPage <= 0 {
|
|
opts.PerPage = model.ViewQueryDefaultPerPage
|
|
} else if opts.PerPage > model.ViewQueryMaxPerPage {
|
|
opts.PerPage = model.ViewQueryMaxPerPage
|
|
}
|
|
|
|
if opts.Page < 0 {
|
|
opts.Page = 0
|
|
}
|
|
|
|
builder := s.viewSelect.
|
|
Where(sq.Eq{"ChannelId": channelID, "DeleteAt": 0}).
|
|
OrderBy("SortOrder ASC", "CreateAt ASC", "Id ASC").
|
|
Limit(uint64(opts.PerPage)).
|
|
Offset(uint64(opts.Page * opts.PerPage))
|
|
|
|
var rows []model.View
|
|
if err := s.GetReplica().SelectBuilder(&rows, builder); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get views for channel %s", channelID)
|
|
}
|
|
|
|
views := make([]*model.View, len(rows))
|
|
for i := range rows {
|
|
views[i] = &rows[i]
|
|
}
|
|
|
|
return views, nil
|
|
}
|
|
|
|
func (s *SqlViewStore) CountForChannel(channelID string, opts model.ViewQueryOpts) (int64, error) {
|
|
if channelID == "" {
|
|
return 0, store.NewErrInvalidInput("View", "channelID", channelID)
|
|
}
|
|
|
|
builder := s.getQueryBuilder().
|
|
Select("COUNT(*)").
|
|
From("Views").
|
|
Where(sq.Eq{"ChannelId": channelID, "DeleteAt": 0})
|
|
|
|
var count int64
|
|
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
|
|
return 0, errors.Wrapf(err, "failed to count views for channel %s", channelID)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s *SqlViewStore) Update(view *model.View) (*model.View, error) {
|
|
view.PreUpdate()
|
|
if err := view.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
builder := s.getQueryBuilder().
|
|
Update("Views").
|
|
Set("Title", view.Title).
|
|
Set("Description", view.Description).
|
|
Set("SortOrder", view.SortOrder).
|
|
Set("Props", view.Props).
|
|
Set("UpdateAt", view.UpdateAt).
|
|
Where(sq.Eq{"Id": view.Id, "DeleteAt": 0})
|
|
|
|
res, err := s.GetMaster().ExecBuilder(builder)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to update view with id=%s", view.Id)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get affected rows after updating view with id=%s", view.Id)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return nil, store.NewErrNotFound("View", view.Id)
|
|
}
|
|
|
|
return view, nil
|
|
}
|
|
|
|
func (s *SqlViewStore) Delete(viewID string, deleteAt int64) error {
|
|
builder := s.getQueryBuilder().
|
|
Update("Views").
|
|
Set("DeleteAt", deleteAt).
|
|
Set("UpdateAt", deleteAt).
|
|
Where(sq.Eq{"Id": viewID, "DeleteAt": 0})
|
|
|
|
res, err := s.GetMaster().ExecBuilder(builder)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to delete view with id=%s", viewID)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get affected rows after deleting view with id=%s", viewID)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return store.NewErrNotFound("View", viewID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlViewStore) UpdateSortOrder(viewID, channelID string, newIndex int64) ([]*model.View, error) {
|
|
if channelID == "" {
|
|
return nil, store.NewErrInvalidInput("View", "channelID", channelID)
|
|
}
|
|
if viewID == "" {
|
|
return nil, store.NewErrInvalidInput("View", "viewID", viewID)
|
|
}
|
|
if newIndex < 0 {
|
|
return nil, store.NewErrInvalidInput("View", "SortOrder", newIndex)
|
|
}
|
|
|
|
now := model.GetMillis()
|
|
transaction, err := s.GetMaster().Begin()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to begin transaction for UpdateSortOrder")
|
|
}
|
|
defer finalizeTransactionX(transaction, &err)
|
|
|
|
builder := s.viewSelect.
|
|
Where(sq.Eq{"ChannelId": channelID, "DeleteAt": 0}).
|
|
OrderBy("SortOrder ASC", "CreateAt ASC", "Id ASC")
|
|
|
|
var rows []model.View
|
|
if err = transaction.SelectBuilder(&rows, builder); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get views for channel %s", channelID)
|
|
}
|
|
|
|
views := make([]*model.View, len(rows))
|
|
for i := range rows {
|
|
views[i] = &rows[i]
|
|
}
|
|
|
|
if len(views) == 0 || int(newIndex) > len(views)-1 {
|
|
return nil, store.NewErrInvalidInput("View", "SortOrder", newIndex)
|
|
}
|
|
|
|
currentIndex := -1
|
|
var current *model.View
|
|
for index, v := range views {
|
|
if v.Id == viewID {
|
|
currentIndex = index
|
|
current = v
|
|
break
|
|
}
|
|
}
|
|
|
|
if currentIndex == -1 {
|
|
return nil, store.NewErrNotFound("View", viewID)
|
|
}
|
|
|
|
views = utils.RemoveElementFromSliceAtIndex(views, currentIndex)
|
|
views = slices.Insert(views, int(newIndex), current)
|
|
|
|
caseStmt := sq.Case()
|
|
query := s.getQueryBuilder().Update("Views")
|
|
ids := make([]string, 0, len(views))
|
|
for index, v := range views {
|
|
v.SortOrder = index
|
|
v.UpdateAt = now
|
|
caseStmt = caseStmt.When(sq.Eq{"Id": v.Id}, strconv.FormatInt(int64(index), 10))
|
|
ids = append(ids, v.Id)
|
|
}
|
|
query = query.Set("SortOrder", caseStmt)
|
|
query = query.Set("UpdateAt", now)
|
|
query = query.Where(sq.Eq{"Id": ids})
|
|
|
|
queryStr, args, queryErr := query.ToSql()
|
|
if queryErr != nil {
|
|
return nil, errors.Wrap(queryErr, "failed to build sort order update query for views")
|
|
}
|
|
|
|
if _, execErr := transaction.Exec(queryStr, args...); execErr != nil {
|
|
return nil, errors.Wrapf(execErr, "failed to update sort order for views in channel %s", channelID)
|
|
}
|
|
|
|
if err = transaction.Commit(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to commit sort order update for views")
|
|
}
|
|
return views, nil
|
|
}
|