github.com/mattermost/mattermost-server/v5@v5.39.3/store/sqlstore/post_store.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package sqlstore 5 6 import ( 7 "context" 8 "database/sql" 9 "fmt" 10 "reflect" 11 "regexp" 12 "strconv" 13 "strings" 14 "sync" 15 16 sq "github.com/Masterminds/squirrel" 17 "github.com/mattermost/gorp" 18 "github.com/pkg/errors" 19 20 "github.com/mattermost/mattermost-server/v5/einterfaces" 21 "github.com/mattermost/mattermost-server/v5/model" 22 "github.com/mattermost/mattermost-server/v5/shared/mlog" 23 "github.com/mattermost/mattermost-server/v5/store" 24 "github.com/mattermost/mattermost-server/v5/store/searchlayer" 25 "github.com/mattermost/mattermost-server/v5/utils" 26 ) 27 28 type SqlPostStore struct { 29 *SqlStore 30 metrics einterfaces.MetricsInterface 31 maxPostSizeOnce sync.Once 32 maxPostSizeCached int 33 } 34 35 type postWithExtra struct { 36 ThreadReplyCount int64 37 IsFollowing *bool 38 ThreadParticipants model.StringArray 39 model.Post 40 } 41 42 func (s *SqlPostStore) ClearCaches() { 43 } 44 45 func postSliceColumnsWithTypes() []struct { 46 Name string 47 Type reflect.Kind 48 } { 49 return []struct { 50 Name string 51 Type reflect.Kind 52 }{ 53 {"Id", reflect.String}, 54 {"CreateAt", reflect.Int64}, 55 {"UpdateAt", reflect.Int64}, 56 {"EditAt", reflect.Int64}, 57 {"DeleteAt", reflect.Int64}, 58 {"IsPinned", reflect.Bool}, 59 {"UserId", reflect.String}, 60 {"ChannelId", reflect.String}, 61 {"RootId", reflect.String}, 62 {"ParentId", reflect.String}, 63 {"OriginalId", reflect.String}, 64 {"Message", reflect.String}, 65 {"Type", reflect.String}, 66 {"Props", reflect.Map}, 67 {"Hashtags", reflect.String}, 68 {"Filenames", reflect.Slice}, 69 {"FileIds", reflect.Slice}, 70 {"HasReactions", reflect.Bool}, 71 {"RemoteId", reflect.String}, 72 } 73 } 74 75 func postToSlice(post *model.Post) []interface{} { 76 return []interface{}{ 77 post.Id, 78 post.CreateAt, 79 post.UpdateAt, 80 post.EditAt, 81 post.DeleteAt, 82 post.IsPinned, 83 post.UserId, 84 post.ChannelId, 85 post.RootId, 86 post.ParentId, 87 post.OriginalId, 88 post.Message, 89 post.Type, 90 model.StringInterfaceToJson(post.Props), 91 post.Hashtags, 92 model.ArrayToJson(post.Filenames), 93 model.ArrayToJson(post.FileIds), 94 post.HasReactions, 95 post.RemoteId, 96 } 97 } 98 99 func postSliceColumns() []string { 100 colInfos := postSliceColumnsWithTypes() 101 cols := make([]string, len(colInfos)) 102 for i, colInfo := range colInfos { 103 cols[i] = colInfo.Name 104 } 105 return cols 106 } 107 108 func postSliceCoalesceQuery() string { 109 colInfos := postSliceColumnsWithTypes() 110 cols := make([]string, len(colInfos)) 111 for i, colInfo := range colInfos { 112 var defaultValue string 113 switch colInfo.Type { 114 case reflect.String: 115 defaultValue = "''" 116 case reflect.Int64: 117 defaultValue = "0" 118 case reflect.Bool: 119 defaultValue = "false" 120 case reflect.Map: 121 defaultValue = "'{}'" 122 case reflect.Slice: 123 defaultValue = "'[]'" 124 } 125 cols[i] = "COALESCE(Posts." + colInfo.Name + "," + defaultValue + ") AS " + colInfo.Name 126 } 127 return strings.Join(cols, ",") 128 } 129 130 func newSqlPostStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.PostStore { 131 s := &SqlPostStore{ 132 SqlStore: sqlStore, 133 metrics: metrics, 134 maxPostSizeCached: model.POST_MESSAGE_MAX_RUNES_V1, 135 } 136 137 for _, db := range sqlStore.GetAllConns() { 138 table := db.AddTableWithName(model.Post{}, "Posts").SetKeys(false, "Id") 139 table.ColMap("Id").SetMaxSize(26) 140 table.ColMap("UserId").SetMaxSize(26) 141 table.ColMap("ChannelId").SetMaxSize(26) 142 table.ColMap("RootId").SetMaxSize(26) 143 table.ColMap("ParentId").SetMaxSize(26) 144 table.ColMap("OriginalId").SetMaxSize(26) 145 table.ColMap("Message").SetMaxSize(model.POST_MESSAGE_MAX_BYTES_V2) 146 table.ColMap("Type").SetMaxSize(26) 147 table.ColMap("Hashtags").SetMaxSize(1000) 148 table.ColMap("Props").SetMaxSize(8000) 149 table.ColMap("Filenames").SetMaxSize(model.POST_FILENAMES_MAX_RUNES) 150 table.ColMap("FileIds").SetMaxSize(300) 151 table.ColMap("RemoteId").SetMaxSize(26) 152 } 153 154 return s 155 } 156 157 func (s *SqlPostStore) createIndexesIfNotExists() { 158 s.CreateIndexIfNotExists("idx_posts_update_at", "Posts", "UpdateAt") 159 s.CreateIndexIfNotExists("idx_posts_create_at", "Posts", "CreateAt") 160 s.CreateIndexIfNotExists("idx_posts_delete_at", "Posts", "DeleteAt") 161 s.CreateIndexIfNotExists("idx_posts_root_id", "Posts", "RootId") 162 s.CreateIndexIfNotExists("idx_posts_user_id", "Posts", "UserId") 163 s.CreateIndexIfNotExists("idx_posts_is_pinned", "Posts", "IsPinned") 164 165 s.CreateCompositeIndexIfNotExists("idx_posts_channel_id_update_at", "Posts", []string{"ChannelId", "UpdateAt"}) 166 s.CreateCompositeIndexIfNotExists("idx_posts_channel_id_delete_at_create_at", "Posts", []string{"ChannelId", "DeleteAt", "CreateAt"}) 167 168 s.CreateFullTextIndexIfNotExists("idx_posts_message_txt", "Posts", "Message") 169 s.CreateFullTextIndexIfNotExists("idx_posts_hashtags_txt", "Posts", "Hashtags") 170 } 171 172 func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) { 173 channelNewPosts := make(map[string]int) 174 channelNewRootPosts := make(map[string]int) 175 maxDateNewPosts := make(map[string]int64) 176 rootIds := make(map[string]int) 177 maxDateRootIds := make(map[string]int64) 178 for idx, post := range posts { 179 if post.Id != "" && !post.IsRemote() { 180 return nil, idx, store.NewErrInvalidInput("Post", "id", post.Id) 181 } 182 post.PreSave() 183 maxPostSize := s.GetMaxPostSize() 184 if err := post.IsValid(maxPostSize); err != nil { 185 return nil, idx, err 186 } 187 188 if currentChannelCount, ok := channelNewPosts[post.ChannelId]; !ok { 189 if post.IsJoinLeaveMessage() { 190 channelNewPosts[post.ChannelId] = 0 191 } else { 192 channelNewPosts[post.ChannelId] = 1 193 } 194 maxDateNewPosts[post.ChannelId] = post.CreateAt 195 } else { 196 if !post.IsJoinLeaveMessage() { 197 channelNewPosts[post.ChannelId] = currentChannelCount + 1 198 } 199 if post.CreateAt > maxDateNewPosts[post.ChannelId] { 200 maxDateNewPosts[post.ChannelId] = post.CreateAt 201 } 202 } 203 204 if post.RootId == "" { 205 if currentChannelCount, ok := channelNewRootPosts[post.ChannelId]; !ok { 206 if post.IsJoinLeaveMessage() { 207 channelNewRootPosts[post.ChannelId] = 0 208 } else { 209 channelNewRootPosts[post.ChannelId] = 1 210 } 211 } else { 212 if !post.IsJoinLeaveMessage() { 213 channelNewRootPosts[post.ChannelId] = currentChannelCount + 1 214 } 215 } 216 continue 217 } 218 219 if currentRootCount, ok := rootIds[post.RootId]; !ok { 220 rootIds[post.RootId] = 1 221 maxDateRootIds[post.RootId] = post.CreateAt 222 } else { 223 rootIds[post.RootId] = currentRootCount + 1 224 if post.CreateAt > maxDateRootIds[post.RootId] { 225 maxDateRootIds[post.RootId] = post.CreateAt 226 } 227 } 228 } 229 230 builder := s.getQueryBuilder().Insert("Posts").Columns(postSliceColumns()...) 231 for _, post := range posts { 232 builder = builder.Values(postToSlice(post)...) 233 } 234 query, args, err := builder.ToSql() 235 if err != nil { 236 return nil, -1, errors.Wrap(err, "post_tosql") 237 } 238 239 transaction, err := s.GetMaster().Begin() 240 if err != nil { 241 return posts, -1, errors.Wrap(err, "begin_transaction") 242 } 243 244 defer finalizeTransaction(transaction) 245 246 if _, err = transaction.Exec(query, args...); err != nil { 247 return nil, -1, errors.Wrap(err, "failed to save Post") 248 } 249 250 if err = s.updateThreadsFromPosts(transaction, posts); err != nil { 251 mlog.Warn("Error updating posts, thread update failed", mlog.Err(err)) 252 } 253 254 if err = transaction.Commit(); err != nil { 255 // don't need to rollback here since the transaction is already closed 256 return posts, -1, errors.Wrap(err, "commit_transaction") 257 } 258 259 for channelId, count := range channelNewPosts { 260 countRoot := channelNewRootPosts[channelId] 261 262 if _, err = s.GetMaster().Exec("UPDATE Channels SET LastPostAt = GREATEST(:LastPostAt, LastPostAt), TotalMsgCount = TotalMsgCount + :Count, TotalMsgCountRoot = TotalMsgCountRoot + :CountRoot WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": maxDateNewPosts[channelId], "ChannelId": channelId, "Count": count, "CountRoot": countRoot}); err != nil { 263 mlog.Warn("Error updating Channel LastPostAt.", mlog.Err(err)) 264 } 265 } 266 267 for rootId := range rootIds { 268 if _, err = s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId", map[string]interface{}{"UpdateAt": maxDateRootIds[rootId], "RootId": rootId}); err != nil { 269 mlog.Warn("Error updating Post UpdateAt.", mlog.Err(err)) 270 } 271 } 272 273 var unknownRepliesPosts []*model.Post 274 for _, post := range posts { 275 if post.RootId == "" { 276 count, ok := rootIds[post.Id] 277 if ok { 278 post.ReplyCount += int64(count) 279 } 280 } else { 281 unknownRepliesPosts = append(unknownRepliesPosts, post) 282 } 283 } 284 285 if len(unknownRepliesPosts) > 0 { 286 if err := s.populateReplyCount(unknownRepliesPosts); err != nil { 287 mlog.Warn("Unable to populate the reply count in some posts.", mlog.Err(err)) 288 } 289 } 290 291 return posts, -1, nil 292 } 293 294 func (s *SqlPostStore) Save(post *model.Post) (*model.Post, error) { 295 posts, _, err := s.SaveMultiple([]*model.Post{post}) 296 if err != nil { 297 return nil, err 298 } 299 return posts[0], nil 300 } 301 302 func (s *SqlPostStore) populateReplyCount(posts []*model.Post) error { 303 rootIds := []string{} 304 for _, post := range posts { 305 rootIds = append(rootIds, post.RootId) 306 } 307 countList := []struct { 308 RootId string 309 Count int64 310 }{} 311 query := s.getQueryBuilder().Select("RootId, COUNT(Id) AS Count").From("Posts").Where(sq.Eq{"RootId": rootIds}).Where(sq.Eq{"DeleteAt": 0}).GroupBy("RootId") 312 313 queryString, args, err := query.ToSql() 314 if err != nil { 315 return errors.Wrap(err, "post_tosql") 316 } 317 _, err = s.GetMaster().Select(&countList, queryString, args...) 318 if err != nil { 319 return errors.Wrap(err, "failed to count Posts") 320 } 321 322 counts := map[string]int64{} 323 for _, count := range countList { 324 counts[count.RootId] = count.Count 325 } 326 327 for _, post := range posts { 328 count, ok := counts[post.RootId] 329 if !ok { 330 post.ReplyCount = 0 331 } 332 post.ReplyCount = count 333 } 334 335 return nil 336 } 337 338 func (s *SqlPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) { 339 newPost.UpdateAt = model.GetMillis() 340 newPost.PreCommit() 341 342 oldPost.DeleteAt = newPost.UpdateAt 343 oldPost.UpdateAt = newPost.UpdateAt 344 oldPost.OriginalId = oldPost.Id 345 oldPost.Id = model.NewId() 346 oldPost.PreCommit() 347 348 maxPostSize := s.GetMaxPostSize() 349 350 if err := newPost.IsValid(maxPostSize); err != nil { 351 return nil, err 352 } 353 354 if _, err := s.GetMaster().Update(newPost); err != nil { 355 return nil, errors.Wrapf(err, "failed to update Post with id=%s", newPost.Id) 356 } 357 358 time := model.GetMillis() 359 s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt WHERE Id = :ChannelId AND LastPostAt < :LastPostAt", map[string]interface{}{"LastPostAt": time, "ChannelId": newPost.ChannelId}) 360 361 if newPost.RootId != "" { 362 s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId AND UpdateAt < :UpdateAt", map[string]interface{}{"UpdateAt": time, "RootId": newPost.RootId}) 363 s.GetMaster().Exec("UPDATE Threads SET LastReplyAt = :UpdateAt WHERE PostId = :RootId", map[string]interface{}{"UpdateAt": time, "RootId": newPost.RootId}) 364 } 365 366 // mark the old post as deleted 367 s.GetMaster().Insert(oldPost) 368 369 return newPost, nil 370 } 371 372 func (s *SqlPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) { 373 updateAt := model.GetMillis() 374 maxPostSize := s.GetMaxPostSize() 375 for idx, post := range posts { 376 post.UpdateAt = updateAt 377 if appErr := post.IsValid(maxPostSize); appErr != nil { 378 return nil, idx, appErr 379 } 380 } 381 382 tx, err := s.GetMaster().Begin() 383 if err != nil { 384 return nil, -1, errors.Wrap(err, "begin_transaction") 385 } 386 for idx, post := range posts { 387 if _, err = tx.Update(post); err != nil { 388 txErr := tx.Rollback() 389 if txErr != nil { 390 return nil, idx, errors.Wrap(txErr, "rollback_transaction") 391 } 392 393 return nil, idx, errors.Wrap(err, "failed to update Post") 394 } 395 if post.RootId != "" { 396 tx.Exec("UPDATE Threads SET LastReplyAt = :UpdateAt WHERE PostId = :RootId", map[string]interface{}{"UpdateAt": updateAt, "RootId": post.Id}) 397 } 398 } 399 err = tx.Commit() 400 if err != nil { 401 return nil, -1, errors.Wrap(err, "commit_transaction") 402 } 403 404 return posts, -1, nil 405 } 406 407 func (s *SqlPostStore) Overwrite(post *model.Post) (*model.Post, error) { 408 posts, _, err := s.OverwriteMultiple([]*model.Post{post}) 409 if err != nil { 410 return nil, err 411 } 412 413 return posts[0], nil 414 } 415 416 func (s *SqlPostStore) GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, error) { 417 pl := model.NewPostList() 418 419 var posts []*model.Post 420 if _, err := s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "Offset": offset, "Limit": limit}); err != nil { 421 return nil, errors.Wrap(err, "failed to find Posts") 422 } 423 424 for _, post := range posts { 425 pl.AddPost(post) 426 pl.AddOrder(post.Id) 427 } 428 429 return pl, nil 430 } 431 432 func (s *SqlPostStore) GetFlaggedPostsForTeam(userId, teamId string, offset int, limit int) (*model.PostList, error) { 433 pl := model.NewPostList() 434 435 var posts []*model.Post 436 437 query := ` 438 SELECT 439 A.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN A.RootId = '' THEN A.Id ELSE A.RootId END) AND Posts.DeleteAt = 0) as ReplyCount 440 FROM 441 (SELECT 442 * 443 FROM 444 Posts 445 WHERE 446 Id 447 IN 448 (SELECT 449 Name 450 FROM 451 Preferences 452 WHERE 453 UserId = :UserId 454 AND Category = :Category) 455 AND DeleteAt = 0 456 ) as A 457 INNER JOIN Channels as B 458 ON B.Id = A.ChannelId 459 WHERE B.TeamId = :TeamId OR B.TeamId = '' 460 ORDER BY CreateAt DESC 461 LIMIT :Limit OFFSET :Offset` 462 463 if _, err := s.GetReplica().Select(&posts, query, map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "Offset": offset, "Limit": limit, "TeamId": teamId}); err != nil { 464 return nil, errors.Wrap(err, "failed to find Posts") 465 } 466 467 for _, post := range posts { 468 pl.AddPost(post) 469 pl.AddOrder(post.Id) 470 } 471 472 return pl, nil 473 } 474 475 func (s *SqlPostStore) GetFlaggedPostsForChannel(userId, channelId string, offset int, limit int) (*model.PostList, error) { 476 pl := model.NewPostList() 477 478 var posts []*model.Post 479 query := ` 480 SELECT 481 *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount 482 FROM Posts p 483 WHERE 484 Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) 485 AND ChannelId = :ChannelId 486 AND DeleteAt = 0 487 ORDER BY CreateAt DESC 488 LIMIT :Limit OFFSET :Offset` 489 490 if _, err := s.GetReplica().Select(&posts, query, map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil { 491 return nil, errors.Wrap(err, "failed to find Posts") 492 } 493 for _, post := range posts { 494 pl.AddPost(post) 495 pl.AddOrder(post.Id) 496 } 497 498 return pl, nil 499 } 500 func (s *SqlPostStore) getPostWithCollapsedThreads(id, userID string, extended bool) (*model.PostList, error) { 501 if id == "" { 502 return nil, store.NewErrInvalidInput("Post", "id", id) 503 } 504 505 var columns []string 506 for _, c := range postSliceColumns() { 507 columns = append(columns, "Posts."+c) 508 } 509 columns = append(columns, 510 "COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount", 511 "COALESCE(Threads.LastReplyAt, 0) as LastReplyAt", 512 "COALESCE(Threads.Participants, '[]') as ThreadParticipants", 513 "COALESCE(ThreadMemberships.Following, false) as IsFollowing", 514 ) 515 var post postWithExtra 516 517 postFetchQuery, args, _ := s.getQueryBuilder(). 518 Select(columns...). 519 From("Posts"). 520 LeftJoin("Threads ON Threads.PostId = Id"). 521 LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", userID). 522 Where(sq.Eq{"DeleteAt": 0}). 523 Where(sq.Eq{"Id": id}).ToSql() 524 525 err := s.GetReplica().SelectOne(&post, postFetchQuery, args...) 526 if err != nil { 527 if err == sql.ErrNoRows { 528 return nil, store.NewErrNotFound("Post", id) 529 } 530 531 return nil, errors.Wrapf(err, "failed to get Post with id=%s", id) 532 } 533 534 var posts []*model.Post 535 _, err = s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE Posts.RootId = :RootId AND DeleteAt = 0", map[string]interface{}{"RootId": id}) 536 if err != nil { 537 return nil, errors.Wrapf(err, "failed to find Posts for thread %s", id) 538 } 539 540 list, err := s.prepareThreadedResponse([]*postWithExtra{&post}, extended, false) 541 if err != nil { 542 return nil, err 543 } 544 for _, p := range posts { 545 list.AddPost(p) 546 list.AddOrder(p.Id) 547 } 548 return list, nil 549 } 550 551 func (s *SqlPostStore) Get(ctx context.Context, id string, skipFetchThreads, collapsedThreads, collapsedThreadsExtended bool, userID string) (*model.PostList, error) { 552 if collapsedThreads { 553 return s.getPostWithCollapsedThreads(id, userID, collapsedThreadsExtended) 554 } 555 pl := model.NewPostList() 556 557 if id == "" { 558 return nil, store.NewErrInvalidInput("Post", "id", id) 559 } 560 561 var post model.Post 562 postFetchQuery := "SELECT p.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id = :Id AND p.DeleteAt = 0" 563 err := s.DBFromContext(ctx).SelectOne(&post, postFetchQuery, map[string]interface{}{"Id": id}) 564 if err != nil { 565 if err == sql.ErrNoRows { 566 return nil, store.NewErrNotFound("Post", id) 567 } 568 569 return nil, errors.Wrapf(err, "failed to get Post with id=%s", id) 570 } 571 pl.AddPost(&post) 572 pl.AddOrder(id) 573 if !skipFetchThreads { 574 rootId := post.RootId 575 576 if rootId == "" { 577 rootId = post.Id 578 } 579 580 if rootId == "" { 581 return nil, errors.Wrapf(err, "invalid rootId with value=%s", rootId) 582 } 583 584 var posts []*model.Post 585 _, err = s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE (Id = :Id OR RootId = :RootId) AND DeleteAt = 0", map[string]interface{}{"Id": rootId, "RootId": rootId}) 586 if err != nil { 587 return nil, errors.Wrap(err, "failed to find Posts") 588 } 589 590 for _, p := range posts { 591 pl.AddPost(p) 592 pl.AddOrder(p.Id) 593 } 594 } 595 return pl, nil 596 } 597 598 func (s *SqlPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) { 599 query := s.getQueryBuilder(). 600 Select("p.*"). 601 From("Posts p"). 602 Where(sq.Eq{"p.Id": id}) 603 604 replyCountSubQuery := s.getQueryBuilder(). 605 Select("COUNT(Posts.Id)"). 606 From("Posts"). 607 Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0")) 608 609 if !inclDeleted { 610 query = query.Where(sq.Eq{"p.DeleteAt": 0}) 611 } 612 query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount")) 613 614 queryString, args, err := query.ToSql() 615 if err != nil { 616 return nil, errors.Wrap(err, "getsingleincldeleted_tosql") 617 } 618 619 var post model.Post 620 err = s.GetReplica().SelectOne(&post, queryString, args...) 621 if err != nil { 622 if err == sql.ErrNoRows { 623 return nil, store.NewErrNotFound("Post", id) 624 } 625 626 return nil, errors.Wrapf(err, "failed to get Post with id=%s", id) 627 } 628 return &post, nil 629 } 630 631 type etagPosts struct { 632 Id string 633 UpdateAt int64 634 } 635 636 //nolint:unparam 637 func (s *SqlPostStore) InvalidateLastPostTimeCache(channelId string) { 638 } 639 640 //nolint:unparam 641 func (s *SqlPostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string { 642 q := s.getQueryBuilder().Select("Id", "UpdateAt").From("Posts").Where(sq.Eq{"ChannelId": channelId}).OrderBy("UpdateAt DESC").Limit(1) 643 if collapsedThreads { 644 q.Where(sq.Eq{"RootId": ""}) 645 } 646 sql, args, _ := q.ToSql() 647 648 var et etagPosts 649 err := s.GetReplica().SelectOne(&et, sql, args...) 650 var result string 651 if err != nil { 652 result = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis()) 653 } else { 654 result = fmt.Sprintf("%v.%v", model.CurrentVersion, et.UpdateAt) 655 } 656 657 return result 658 } 659 660 func (s *SqlPostStore) Delete(postId string, time int64, deleteByID string) error { 661 var post model.Post 662 err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": postId}) 663 if err != nil { 664 if err == sql.ErrNoRows { 665 return store.NewErrNotFound("Post", postId) 666 } 667 668 return errors.Wrapf(err, "failed to delete Post with id=%s", postId) 669 } 670 671 post.AddProp(model.POST_PROPS_DELETE_BY, deleteByID) 672 673 _, err = s.GetMaster().Exec("UPDATE Posts SET DeleteAt = :DeleteAt, UpdateAt = :UpdateAt, Props = :Props WHERE Id = :Id OR RootId = :RootId", map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": postId, "RootId": postId, "Props": model.StringInterfaceToJson(post.GetProps())}) 674 if err != nil { 675 return errors.Wrap(err, "failed to update Posts") 676 } 677 678 return s.cleanupThreads(post.Id, post.RootId, false) 679 } 680 681 func (s *SqlPostStore) permanentDelete(postId string) error { 682 var post model.Post 683 err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": postId}) 684 if err != nil && err != sql.ErrNoRows { 685 return errors.Wrapf(err, "failed to get Post with id=%s", postId) 686 } 687 if err = s.cleanupThreads(post.Id, post.RootId, true); err != nil { 688 return errors.Wrapf(err, "failed to cleanup threads for Post with id=%s", postId) 689 } 690 691 if _, err = s.GetMaster().Exec("DELETE FROM Posts WHERE Id = :Id OR RootId = :RootId", map[string]interface{}{"Id": postId, "RootId": postId}); err != nil { 692 return errors.Wrapf(err, "failed to delete Post with id=%s", postId) 693 } 694 695 return nil 696 } 697 698 type postIds struct { 699 Id string 700 RootId string 701 UserId string 702 } 703 704 func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) error { 705 results := []postIds{} 706 _, err := s.GetMaster().Select(&results, "Select Id, RootId FROM Posts WHERE UserId = :UserId AND RootId != ''", map[string]interface{}{"UserId": userId}) 707 if err != nil { 708 return errors.Wrapf(err, "failed to fetch Posts with userId=%s", userId) 709 } 710 711 for _, ids := range results { 712 if err = s.cleanupThreads(ids.Id, ids.RootId, true); err != nil { 713 return err 714 } 715 } 716 717 _, err = s.GetMaster().Exec("DELETE FROM Posts WHERE UserId = :UserId AND RootId != ''", map[string]interface{}{"UserId": userId}) 718 if err != nil { 719 return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId) 720 } 721 return nil 722 } 723 724 func (s *SqlPostStore) PermanentDeleteByUser(userId string) error { 725 // First attempt to delete all the comments for a user 726 if err := s.permanentDeleteAllCommentByUser(userId); err != nil { 727 return err 728 } 729 730 // Now attempt to delete all the root posts for a user. This will also 731 // delete all the comments for each post 732 found := true 733 count := 0 734 735 for found { 736 var ids []string 737 _, err := s.GetMaster().Select(&ids, "SELECT Id FROM Posts WHERE UserId = :UserId LIMIT 1000", map[string]interface{}{"UserId": userId}) 738 if err != nil { 739 return errors.Wrapf(err, "failed to find Posts with userId=%s", userId) 740 } 741 742 found = false 743 for _, id := range ids { 744 found = true 745 if err = s.permanentDelete(id); err != nil { 746 return err 747 } 748 } 749 750 // This is a fail safe, give up if more than 10k messages 751 count++ 752 if count >= 10 { 753 return errors.Wrapf(err, "too many Posts to delete with userId=%s", userId) 754 } 755 } 756 757 return nil 758 } 759 760 func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) error { 761 results := []postIds{} 762 _, err := s.GetMaster().Select(&results, "SELECT Id, RootId, UserId FROM Posts WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId}) 763 if err != nil { 764 return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId) 765 } 766 767 for _, ids := range results { 768 if err = s.cleanupThreads(ids.Id, ids.RootId, true); err != nil { 769 return err 770 } 771 } 772 773 if _, err := s.GetMaster().Exec("DELETE FROM Posts WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId}); err != nil { 774 return errors.Wrapf(err, "failed to delete Posts with channelId=%s", channelId) 775 } 776 return nil 777 } 778 779 func (s *SqlPostStore) prepareThreadedResponse(posts []*postWithExtra, extended, reversed bool) (*model.PostList, error) { 780 list := model.NewPostList() 781 var userIds []string 782 userIdMap := map[string]bool{} 783 for _, thread := range posts { 784 for _, participantId := range thread.ThreadParticipants { 785 if _, ok := userIdMap[participantId]; !ok { 786 userIdMap[participantId] = true 787 userIds = append(userIds, participantId) 788 } 789 } 790 } 791 var users []*model.User 792 if extended { 793 var err error 794 users, err = s.User().GetProfileByIds(context.Background(), userIds, &store.UserGetByIdsOpts{}, true) 795 if err != nil { 796 return nil, err 797 } 798 } else { 799 for _, userId := range userIds { 800 users = append(users, &model.User{Id: userId}) 801 } 802 } 803 processPost := func(p *postWithExtra) error { 804 p.Post.ReplyCount = p.ThreadReplyCount 805 if p.IsFollowing != nil { 806 p.Post.IsFollowing = model.NewBool(*p.IsFollowing) 807 } 808 for _, th := range p.ThreadParticipants { 809 var participant *model.User 810 for _, u := range users { 811 if u.Id == th { 812 participant = u 813 break 814 } 815 } 816 if participant == nil { 817 return errors.New("cannot find thread participant with id=" + th) 818 } 819 p.Post.Participants = append(p.Post.Participants, participant) 820 } 821 return nil 822 } 823 824 l := len(posts) 825 for i := range posts { 826 idx := i 827 // We need to flip the order if we selected backwards 828 829 if reversed { 830 idx = l - i - 1 831 } 832 if err := processPost(posts[idx]); err != nil { 833 return nil, err 834 } 835 list.AddPost(&posts[idx].Post) 836 list.AddOrder(posts[idx].Id) 837 } 838 839 return list, nil 840 } 841 842 func (s *SqlPostStore) getPostsCollapsedThreads(options model.GetPostsOptions) (*model.PostList, error) { 843 var columns []string 844 for _, c := range postSliceColumns() { 845 columns = append(columns, "Posts."+c) 846 } 847 columns = append(columns, 848 "COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount", 849 "COALESCE(Threads.LastReplyAt, 0) as LastReplyAt", 850 "COALESCE(Threads.Participants, '[]') as ThreadParticipants", 851 "COALESCE(ThreadMemberships.Following, false) as IsFollowing", 852 ) 853 var posts []*postWithExtra 854 offset := options.PerPage * options.Page 855 856 postFetchQuery, args, _ := s.getQueryBuilder(). 857 Select(columns...). 858 From("Posts"). 859 LeftJoin("Threads ON Threads.PostId = Id"). 860 LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", options.UserId). 861 Where(sq.Eq{"DeleteAt": 0}). 862 Where(sq.Eq{"Posts.ChannelId": options.ChannelId}). 863 Where(sq.Eq{"RootId": ""}). 864 Limit(uint64(options.PerPage)). 865 Offset(uint64(offset)). 866 OrderBy("CreateAt DESC").ToSql() 867 868 _, err := s.GetReplica().Select(&posts, postFetchQuery, args...) 869 870 if err != nil { 871 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId) 872 } 873 874 return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false) 875 } 876 877 func (s *SqlPostStore) GetPosts(options model.GetPostsOptions, _ bool) (*model.PostList, error) { 878 if options.PerPage > 1000 { 879 return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage) 880 } 881 if options.CollapsedThreads { 882 return s.getPostsCollapsedThreads(options) 883 } 884 offset := options.PerPage * options.Page 885 886 rpc := make(chan store.StoreResult, 1) 887 go func() { 888 posts, err := s.getRootPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads) 889 rpc <- store.StoreResult{Data: posts, NErr: err} 890 close(rpc) 891 }() 892 cpc := make(chan store.StoreResult, 1) 893 go func() { 894 posts, err := s.getParentsPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads) 895 cpc <- store.StoreResult{Data: posts, NErr: err} 896 close(cpc) 897 }() 898 899 list := model.NewPostList() 900 901 rpr := <-rpc 902 if rpr.NErr != nil { 903 return nil, rpr.NErr 904 } 905 906 cpr := <-cpc 907 if cpr.NErr != nil { 908 return nil, cpr.NErr 909 } 910 911 posts := rpr.Data.([]*model.Post) 912 parents := cpr.Data.([]*model.Post) 913 914 for _, p := range posts { 915 list.AddPost(p) 916 list.AddOrder(p.Id) 917 } 918 919 for _, p := range parents { 920 list.AddPost(p) 921 } 922 923 list.MakeNonNil() 924 925 return list, nil 926 } 927 928 func (s *SqlPostStore) getPostsSinceCollapsedThreads(options model.GetPostsSinceOptions) (*model.PostList, error) { 929 var columns []string 930 for _, c := range postSliceColumns() { 931 columns = append(columns, "Posts."+c) 932 } 933 columns = append(columns, 934 "COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount", 935 "COALESCE(Threads.LastReplyAt, 0) as LastReplyAt", 936 "COALESCE(Threads.Participants, '[]') as ThreadParticipants", 937 "COALESCE(ThreadMemberships.Following, false) as IsFollowing", 938 ) 939 var posts []*postWithExtra 940 941 postFetchQuery, args, _ := s.getQueryBuilder(). 942 Select(columns...). 943 From("Posts"). 944 LeftJoin("Threads ON Threads.PostId = Id"). 945 LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", options.UserId). 946 Where(sq.Eq{"DeleteAt": 0}). 947 Where(sq.Eq{"Posts.ChannelId": options.ChannelId}). 948 Where(sq.Gt{"UpdateAt": options.Time}). 949 Where(sq.Eq{"RootId": ""}). 950 OrderBy("CreateAt DESC").ToSql() 951 952 _, err := s.GetReplica().Select(&posts, postFetchQuery, args...) 953 954 if err != nil { 955 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId) 956 } 957 return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false) 958 } 959 960 //nolint:unparam 961 func (s *SqlPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool) (*model.PostList, error) { 962 if options.CollapsedThreads { 963 return s.getPostsSinceCollapsedThreads(options) 964 } 965 966 var posts []*model.Post 967 968 order := "DESC" 969 if options.SortAscending { 970 order = "ASC" 971 } 972 973 replyCountQuery1 := "" 974 replyCountQuery2 := "" 975 if options.SkipFetchThreads { 976 replyCountQuery1 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p1.RootId = '' THEN p1.Id ELSE p1.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` 977 replyCountQuery2 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN cte.RootId = '' THEN cte.Id ELSE cte.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` 978 } 979 var query string 980 981 // union of IDs and then join to get full posts is faster in mysql 982 if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 983 query = `SELECT *` + replyCountQuery1 + ` FROM Posts p1 JOIN ( 984 (SELECT 985 Id 986 FROM 987 Posts p2 988 WHERE 989 (UpdateAt > :Time 990 AND ChannelId = :ChannelId) 991 LIMIT 1000) 992 UNION 993 (SELECT 994 Id 995 FROM 996 Posts p3 997 WHERE 998 Id 999 IN 1000 (SELECT * FROM (SELECT 1001 RootId 1002 FROM 1003 Posts 1004 WHERE 1005 UpdateAt > :Time 1006 AND ChannelId = :ChannelId 1007 LIMIT 1000) temp_tab)) 1008 ) j ON p1.Id = j.Id 1009 ORDER BY CreateAt ` + order 1010 } else if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 1011 query = `WITH cte AS (SELECT 1012 * 1013 FROM 1014 Posts 1015 WHERE 1016 UpdateAt > :Time AND ChannelId = :ChannelId 1017 LIMIT 1000) 1018 (SELECT *` + replyCountQuery2 + ` FROM cte) 1019 UNION 1020 (SELECT *` + replyCountQuery1 + ` FROM Posts p1 WHERE id in (SELECT rootid FROM cte)) 1021 ORDER BY CreateAt ` + order 1022 } 1023 _, err := s.GetReplica().Select(&posts, query, map[string]interface{}{"ChannelId": options.ChannelId, "Time": options.Time}) 1024 1025 if err != nil { 1026 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId) 1027 } 1028 1029 list := model.NewPostList() 1030 1031 for _, p := range posts { 1032 list.AddPost(p) 1033 if p.UpdateAt > options.Time { 1034 list.AddOrder(p.Id) 1035 } 1036 } 1037 1038 return list, nil 1039 } 1040 1041 func (s *SqlPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) { 1042 query := ` 1043 SELECT EXISTS (SELECT 1 1044 FROM 1045 Posts 1046 WHERE 1047 UpdateAt >= :Time 1048 AND 1049 ChannelId = :ChannelId 1050 AND 1051 UserId = :UserId 1052 AND 1053 Type = :Type 1054 LIMIT 1)` 1055 1056 var exist bool 1057 err := s.GetReplica().SelectOne(&exist, query, map[string]interface{}{ 1058 "ChannelId": options.ChannelId, 1059 "Time": options.Time, 1060 "UserId": userId, 1061 "Type": model.POST_AUTO_RESPONDER, 1062 }) 1063 1064 if err != nil { 1065 return false, errors.Wrapf(err, 1066 "failed to check if autoresponse posts in channelId=%s for userId=%s since %s", options.ChannelId, userId, model.GetTimeForMillis(options.Time)) 1067 } 1068 1069 return exist, nil 1070 } 1071 1072 func (s *SqlPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) { 1073 query := s.getQueryBuilder(). 1074 Select("*"). 1075 From("Posts"). 1076 Where(sq.Or{sq.Gt{"UpdateAt": cursor.LastPostUpdateAt}, sq.And{sq.Eq{"UpdateAt": cursor.LastPostUpdateAt}, sq.Gt{"Id": cursor.LastPostId}}}). 1077 OrderBy("UpdateAt", "Id"). 1078 Limit(uint64(limit)) 1079 1080 if options.ChannelId != "" { 1081 query = query.Where(sq.Eq{"ChannelId": options.ChannelId}) 1082 } 1083 1084 if !options.IncludeDeleted { 1085 query = query.Where(sq.Eq{"DeleteAt": 0}) 1086 } 1087 1088 if options.ExcludeRemoteId != "" { 1089 query = query.Where(sq.NotEq{"COALESCE(Posts.RemoteId,'')": options.ExcludeRemoteId}) 1090 } 1091 1092 queryString, args, err := query.ToSql() 1093 if err != nil { 1094 return nil, cursor, errors.Wrap(err, "getpostssinceforsync_tosql") 1095 } 1096 1097 var posts []*model.Post 1098 _, err = s.GetReplica().Select(&posts, queryString, args...) 1099 if err != nil { 1100 return nil, cursor, errors.Wrapf(err, "error getting Posts with channelId=%s", options.ChannelId) 1101 } 1102 1103 if len(posts) != 0 { 1104 cursor.LastPostUpdateAt = posts[len(posts)-1].UpdateAt 1105 cursor.LastPostId = posts[len(posts)-1].Id 1106 } 1107 return posts, cursor, nil 1108 } 1109 1110 func (s *SqlPostStore) GetPostsBefore(options model.GetPostsOptions) (*model.PostList, error) { 1111 return s.getPostsAround(true, options) 1112 } 1113 1114 func (s *SqlPostStore) GetPostsAfter(options model.GetPostsOptions) (*model.PostList, error) { 1115 return s.getPostsAround(false, options) 1116 } 1117 1118 func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions) (*model.PostList, error) { 1119 if options.Page < 0 { 1120 return nil, store.NewErrInvalidInput("Post", "<options.Page>", options.Page) 1121 } 1122 1123 if options.PerPage < 0 { 1124 return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage) 1125 } 1126 1127 offset := options.Page * options.PerPage 1128 var posts []*postWithExtra 1129 var parents []*model.Post 1130 1131 var direction string 1132 var sort string 1133 if before { 1134 direction = "<" 1135 sort = "DESC" 1136 } else { 1137 direction = ">" 1138 sort = "ASC" 1139 } 1140 table := "Posts p" 1141 // We force MySQL to use the right index to prevent it from accidentally 1142 // using the index_merge_intersection optimization. 1143 // See MM-27575. 1144 if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 1145 table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" 1146 } 1147 columns := []string{"p.*"} 1148 if options.CollapsedThreads { 1149 columns = append(columns, 1150 "COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount", 1151 "COALESCE(Threads.LastReplyAt, 0) as LastReplyAt", 1152 "COALESCE(Threads.Participants, '[]') as ThreadParticipants", 1153 "COALESCE(ThreadMemberships.Following, false) as IsFollowing", 1154 ) 1155 } 1156 query := s.getQueryBuilder().Select(columns...) 1157 replyCountSubQuery := s.getQueryBuilder().Select("COUNT(Posts.Id)").From("Posts").Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0")) 1158 1159 conditions := sq.And{ 1160 sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId), 1161 sq.Eq{"p.ChannelId": options.ChannelId}, 1162 sq.Eq{"DeleteAt": int(0)}, 1163 } 1164 if options.CollapsedThreads { 1165 conditions = append(conditions, sq.Eq{"RootId": ""}) 1166 query = query.LeftJoin("Threads ON Threads.PostId = p.Id").LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = p.Id AND ThreadMemberships.UserId=?", options.UserId) 1167 } else { 1168 query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount")) 1169 } 1170 query = query.From(table). 1171 Where(conditions). 1172 // Adding ChannelId and DeleteAt order columns 1173 // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. 1174 // See MM-24170. 1175 OrderBy("p.ChannelId", "DeleteAt", "CreateAt "+sort). 1176 Limit(uint64(options.PerPage)). 1177 Offset(uint64(offset)) 1178 1179 queryString, args, err := query.ToSql() 1180 if err != nil { 1181 return nil, errors.Wrap(err, "post_tosql") 1182 } 1183 _, err = s.GetMaster().Select(&posts, queryString, args...) 1184 if err != nil { 1185 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId) 1186 } 1187 1188 if !options.CollapsedThreads && len(posts) > 0 { 1189 rootIds := []string{} 1190 for _, post := range posts { 1191 rootIds = append(rootIds, post.Id) 1192 if post.RootId != "" { 1193 rootIds = append(rootIds, post.RootId) 1194 } 1195 } 1196 rootQuery := s.getQueryBuilder().Select("p.*") 1197 idQuery := sq.Or{ 1198 sq.Eq{"Id": rootIds}, 1199 } 1200 rootQuery = rootQuery.Column(sq.Alias(replyCountSubQuery, "ReplyCount")) 1201 if !options.SkipFetchThreads { 1202 idQuery = append(idQuery, sq.Eq{"RootId": rootIds}) // preserve original behaviour 1203 } 1204 1205 rootQuery = rootQuery.From("Posts p"). 1206 Where(sq.And{ 1207 idQuery, 1208 sq.Eq{"ChannelId": options.ChannelId}, 1209 sq.Eq{"DeleteAt": 0}, 1210 }). 1211 OrderBy("CreateAt DESC") 1212 1213 rootQueryString, rootArgs, nErr := rootQuery.ToSql() 1214 1215 if nErr != nil { 1216 return nil, errors.Wrap(nErr, "post_tosql") 1217 } 1218 _, nErr = s.GetMaster().Select(&parents, rootQueryString, rootArgs...) 1219 if nErr != nil { 1220 return nil, errors.Wrapf(nErr, "failed to find Posts with channelId=%s", options.ChannelId) 1221 } 1222 } 1223 1224 list, err := s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, !before) 1225 if err != nil { 1226 return nil, err 1227 } 1228 1229 for _, p := range parents { 1230 list.AddPost(p) 1231 } 1232 1233 return list, nil 1234 } 1235 1236 func (s *SqlPostStore) GetPostIdBeforeTime(channelId string, time int64, collapsedThreads bool) (string, error) { 1237 return s.getPostIdAroundTime(channelId, time, true, collapsedThreads) 1238 } 1239 1240 func (s *SqlPostStore) GetPostIdAfterTime(channelId string, time int64, collapsedThreads bool) (string, error) { 1241 return s.getPostIdAroundTime(channelId, time, false, collapsedThreads) 1242 } 1243 1244 func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before bool, collapsedThreads bool) (string, error) { 1245 var direction sq.Sqlizer 1246 var sort string 1247 if before { 1248 direction = sq.Lt{"CreateAt": time} 1249 sort = "DESC" 1250 } else { 1251 direction = sq.Gt{"CreateAt": time} 1252 sort = "ASC" 1253 } 1254 1255 table := "Posts" 1256 // We force MySQL to use the right index to prevent it from accidentally 1257 // using the index_merge_intersection optimization. 1258 // See MM-27575. 1259 if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 1260 table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" 1261 } 1262 1263 conditions := sq.And{ 1264 direction, 1265 sq.Eq{"ChannelId": channelId}, 1266 sq.Eq{"DeleteAt": int(0)}, 1267 } 1268 if collapsedThreads { 1269 conditions = sq.And{conditions, sq.Eq{"RootId": ""}} 1270 } 1271 query := s.getQueryBuilder(). 1272 Select("Id"). 1273 From(table). 1274 Where(conditions). 1275 // Adding ChannelId and DeleteAt order columns 1276 // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. 1277 // See MM-23369. 1278 OrderBy("ChannelId", "DeleteAt", "CreateAt "+sort). 1279 Limit(1) 1280 1281 queryString, args, err := query.ToSql() 1282 if err != nil { 1283 return "", errors.Wrap(err, "post_tosql") 1284 } 1285 1286 var postId string 1287 if err := s.GetMaster().SelectOne(&postId, queryString, args...); err != nil { 1288 if err != sql.ErrNoRows { 1289 return "", errors.Wrapf(err, "failed to get Post id with channelId=%s", channelId) 1290 } 1291 } 1292 1293 return postId, nil 1294 } 1295 1296 func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64, collapsedThreads bool) (*model.Post, error) { 1297 table := "Posts" 1298 // We force MySQL to use the right index to prevent it from accidentally 1299 // using the index_merge_intersection optimization. 1300 // See MM-27575. 1301 if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 1302 table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" 1303 } 1304 conditions := sq.And{ 1305 sq.Gt{"CreateAt": time}, 1306 sq.Eq{"ChannelId": channelId}, 1307 sq.Eq{"DeleteAt": int(0)}, 1308 } 1309 if collapsedThreads { 1310 conditions = sq.And{conditions, sq.Eq{"RootId": ""}} 1311 } 1312 query := s.getQueryBuilder(). 1313 Select("*"). 1314 From(table). 1315 Where(conditions). 1316 // Adding ChannelId and DeleteAt order columns 1317 // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. 1318 // See MM-23369. 1319 OrderBy("ChannelId", "DeleteAt", "CreateAt ASC"). 1320 Limit(1) 1321 1322 queryString, args, err := query.ToSql() 1323 if err != nil { 1324 return nil, errors.Wrap(err, "post_tosql") 1325 } 1326 1327 var post *model.Post 1328 if err := s.GetMaster().SelectOne(&post, queryString, args...); err != nil { 1329 if err != sql.ErrNoRows { 1330 return nil, errors.Wrapf(err, "failed to get Post with channelId=%s", channelId) 1331 } 1332 } 1333 1334 return post, nil 1335 } 1336 1337 func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int, skipFetchThreads bool) ([]*model.Post, error) { 1338 var posts []*model.Post 1339 var fetchQuery string 1340 if skipFetchThreads { 1341 fetchQuery = "SELECT p.*, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset" 1342 } else { 1343 fetchQuery = "SELECT * FROM Posts WHERE ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset" 1344 } 1345 _, err := s.GetReplica().Select(&posts, fetchQuery, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}) 1346 if err != nil { 1347 return nil, errors.Wrap(err, "failed to find Posts") 1348 } 1349 return posts, nil 1350 } 1351 1352 func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int, skipFetchThreads bool) ([]*model.Post, error) { 1353 if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 1354 return s.getParentsPostsPostgreSQL(channelId, offset, limit, skipFetchThreads) 1355 } 1356 1357 // query parent Ids first 1358 var roots []*struct { 1359 RootId string 1360 } 1361 rootQuery := ` 1362 SELECT DISTINCT 1363 q.RootId 1364 FROM 1365 (SELECT 1366 RootId 1367 FROM 1368 Posts 1369 WHERE 1370 ChannelId = :ChannelId 1371 AND DeleteAt = 0 1372 ORDER BY CreateAt DESC 1373 LIMIT :Limit OFFSET :Offset) q 1374 WHERE q.RootId != ''` 1375 1376 _, err := s.GetReplica().Select(&roots, rootQuery, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}) 1377 if err != nil { 1378 return nil, errors.Wrap(err, "failed to find Posts") 1379 } 1380 if len(roots) == 0 { 1381 return nil, nil 1382 } 1383 params := make(map[string]interface{}) 1384 placeholders := make([]string, len(roots)) 1385 for idx, r := range roots { 1386 key := fmt.Sprintf(":Root%v", idx) 1387 params[key[1:]] = r.RootId 1388 placeholders[idx] = key 1389 } 1390 placeholderString := strings.Join(placeholders, ", ") 1391 params["ChannelId"] = channelId 1392 replyCountQuery := "" 1393 whereStatement := "p.Id IN (" + placeholderString + ")" 1394 if skipFetchThreads { 1395 replyCountQuery = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` 1396 } else { 1397 whereStatement += " OR p.RootId IN (" + placeholderString + ")" 1398 } 1399 var posts []*model.Post 1400 _, err = s.GetReplica().Select(&posts, ` 1401 SELECT p.*`+replyCountQuery+` 1402 FROM 1403 Posts p 1404 WHERE 1405 (`+whereStatement+`) 1406 AND ChannelId = :ChannelId 1407 AND DeleteAt = 0 1408 ORDER BY CreateAt`, 1409 params) 1410 if err != nil { 1411 return nil, errors.Wrap(err, "failed to find Posts") 1412 } 1413 return posts, nil 1414 } 1415 1416 func (s *SqlPostStore) getParentsPostsPostgreSQL(channelId string, offset int, limit int, skipFetchThreads bool) ([]*model.Post, error) { 1417 var posts []*model.Post 1418 replyCountQuery := "" 1419 onStatement := "q1.RootId = q2.Id" 1420 if skipFetchThreads { 1421 replyCountQuery = ` ,(SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` 1422 } else { 1423 onStatement += " OR q1.RootId = q2.RootId" 1424 } 1425 _, err := s.GetReplica().Select(&posts, 1426 `SELECT q2.*`+replyCountQuery+` 1427 FROM 1428 Posts q2 1429 INNER JOIN 1430 (SELECT DISTINCT 1431 q3.RootId 1432 FROM 1433 (SELECT 1434 RootId 1435 FROM 1436 Posts 1437 WHERE 1438 ChannelId = :ChannelId1 1439 AND DeleteAt = 0 1440 ORDER BY CreateAt DESC 1441 LIMIT :Limit OFFSET :Offset) q3 1442 WHERE q3.RootId != '') q1 1443 ON `+onStatement+` 1444 WHERE 1445 ChannelId = :ChannelId2 1446 AND DeleteAt = 0 1447 ORDER BY CreateAt`, 1448 map[string]interface{}{"ChannelId1": channelId, "Offset": offset, "Limit": limit, "ChannelId2": channelId}) 1449 if err != nil { 1450 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId) 1451 } 1452 return posts, nil 1453 } 1454 1455 var specialSearchChar = []string{ 1456 "<", 1457 ">", 1458 "+", 1459 "-", 1460 "(", 1461 ")", 1462 "~", 1463 "@", 1464 ":", 1465 } 1466 1467 func (s *SqlPostStore) buildCreateDateFilterClause(params *model.SearchParams, queryParams map[string]interface{}) (string, map[string]interface{}) { 1468 searchQuery := "" 1469 // handle after: before: on: filters 1470 if params.OnDate != "" { 1471 onDateStart, onDateEnd := params.GetOnDateMillis() 1472 queryParams["OnDateStart"] = strconv.FormatInt(onDateStart, 10) 1473 queryParams["OnDateEnd"] = strconv.FormatInt(onDateEnd, 10) 1474 1475 // between `on date` start of day and end of day 1476 searchQuery += "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd " 1477 } else { 1478 1479 if params.ExcludedDate != "" { 1480 excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis() 1481 queryParams["ExcludedDateStart"] = strconv.FormatInt(excludedDateStart, 10) 1482 queryParams["ExcludedDateEnd"] = strconv.FormatInt(excludedDateEnd, 10) 1483 1484 searchQuery += "AND CreateAt NOT BETWEEN :ExcludedDateStart AND :ExcludedDateEnd " 1485 } 1486 1487 if params.AfterDate != "" { 1488 afterDate := params.GetAfterDateMillis() 1489 queryParams["AfterDate"] = strconv.FormatInt(afterDate, 10) 1490 1491 // greater than `after date` 1492 searchQuery += "AND CreateAt >= :AfterDate " 1493 } 1494 1495 if params.BeforeDate != "" { 1496 beforeDate := params.GetBeforeDateMillis() 1497 queryParams["BeforeDate"] = strconv.FormatInt(beforeDate, 10) 1498 1499 // less than `before date` 1500 searchQuery += "AND CreateAt <= :BeforeDate " 1501 } 1502 1503 if params.ExcludedAfterDate != "" { 1504 afterDate := params.GetExcludedAfterDateMillis() 1505 queryParams["ExcludedAfterDate"] = strconv.FormatInt(afterDate, 10) 1506 1507 searchQuery += "AND CreateAt < :ExcludedAfterDate " 1508 } 1509 1510 if params.ExcludedBeforeDate != "" { 1511 beforeDate := params.GetExcludedBeforeDateMillis() 1512 queryParams["ExcludedBeforeDate"] = strconv.FormatInt(beforeDate, 10) 1513 1514 searchQuery += "AND CreateAt > :ExcludedBeforeDate " 1515 } 1516 } 1517 1518 return searchQuery, queryParams 1519 } 1520 1521 func (s *SqlPostStore) buildSearchChannelFilterClause(channels []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}, byName bool) (string, map[string]interface{}) { 1522 if len(channels) == 0 { 1523 return "", queryParams 1524 } 1525 1526 clauseSlice := []string{} 1527 for i, channel := range channels { 1528 paramName := paramPrefix + strconv.FormatInt(int64(i), 10) 1529 clauseSlice = append(clauseSlice, ":"+paramName) 1530 queryParams[paramName] = channel 1531 } 1532 clause := strings.Join(clauseSlice, ", ") 1533 if byName { 1534 if exclusion { 1535 return "AND Name NOT IN (" + clause + ")", queryParams 1536 } 1537 return "AND Name IN (" + clause + ")", queryParams 1538 } 1539 1540 if exclusion { 1541 return "AND Id NOT IN (" + clause + ")", queryParams 1542 } 1543 return "AND Id IN (" + clause + ")", queryParams 1544 } 1545 1546 func (s *SqlPostStore) buildSearchUserFilterClause(users []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}, byUsername bool) (string, map[string]interface{}) { 1547 if len(users) == 0 { 1548 return "", queryParams 1549 } 1550 clauseSlice := []string{} 1551 for i, user := range users { 1552 paramName := paramPrefix + strconv.FormatInt(int64(i), 10) 1553 clauseSlice = append(clauseSlice, ":"+paramName) 1554 queryParams[paramName] = user 1555 } 1556 clause := strings.Join(clauseSlice, ", ") 1557 if byUsername { 1558 if exclusion { 1559 return "AND Username NOT IN (" + clause + ")", queryParams 1560 } 1561 return "AND Username IN (" + clause + ")", queryParams 1562 } 1563 if exclusion { 1564 return "AND Id NOT IN (" + clause + ")", queryParams 1565 } 1566 return "AND Id IN (" + clause + ")", queryParams 1567 } 1568 1569 func (s *SqlPostStore) buildSearchPostFilterClause(fromUsers []string, excludedUsers []string, queryParams map[string]interface{}, userByUsername bool) (string, map[string]interface{}) { 1570 if len(fromUsers) == 0 && len(excludedUsers) == 0 { 1571 return "", queryParams 1572 } 1573 1574 filterQuery := ` 1575 AND UserId IN ( 1576 SELECT 1577 Id 1578 FROM 1579 Users, 1580 TeamMembers 1581 WHERE 1582 TeamMembers.TeamId = :TeamId 1583 AND Users.Id = TeamMembers.UserId 1584 FROM_USER_FILTER 1585 EXCLUDED_USER_FILTER)` 1586 1587 fromUserClause, queryParams := s.buildSearchUserFilterClause(fromUsers, "FromUser", false, queryParams, userByUsername) 1588 filterQuery = strings.Replace(filterQuery, "FROM_USER_FILTER", fromUserClause, 1) 1589 1590 excludedUserClause, queryParams := s.buildSearchUserFilterClause(excludedUsers, "ExcludedUser", true, queryParams, userByUsername) 1591 filterQuery = strings.Replace(filterQuery, "EXCLUDED_USER_FILTER", excludedUserClause, 1) 1592 1593 return filterQuery, queryParams 1594 } 1595 1596 func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, error) { 1597 return s.search(teamId, userId, params, true, true) 1598 } 1599 1600 func (s *SqlPostStore) search(teamId string, userId string, params *model.SearchParams, channelsByName bool, userByUsername bool) (*model.PostList, error) { 1601 queryParams := map[string]interface{}{ 1602 "TeamId": teamId, 1603 "UserId": userId, 1604 } 1605 1606 list := model.NewPostList() 1607 if params.Terms == "" && params.ExcludedTerms == "" && 1608 len(params.InChannels) == 0 && len(params.ExcludedChannels) == 0 && 1609 len(params.FromUsers) == 0 && len(params.ExcludedUsers) == 0 && 1610 params.OnDate == "" && params.AfterDate == "" && params.BeforeDate == "" { 1611 return list, nil 1612 } 1613 1614 var posts []*model.Post 1615 1616 deletedQueryPart := "AND DeleteAt = 0" 1617 if params.IncludeDeletedChannels { 1618 deletedQueryPart = "" 1619 } 1620 1621 userIdPart := "AND UserId = :UserId" 1622 if params.SearchWithoutUserId { 1623 userIdPart = "" 1624 } 1625 1626 searchQuery := ` 1627 SELECT 1628 * ,(SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount 1629 FROM 1630 Posts q2 1631 WHERE 1632 DeleteAt = 0 1633 AND Type NOT LIKE '` + model.POST_SYSTEM_MESSAGE_PREFIX + `%' 1634 POST_FILTER 1635 AND ChannelId IN ( 1636 SELECT 1637 Id 1638 FROM 1639 Channels, 1640 ChannelMembers 1641 WHERE 1642 Id = ChannelId 1643 AND (TeamId = :TeamId OR TeamId = '') 1644 ` + userIdPart + ` 1645 ` + deletedQueryPart + ` 1646 IN_CHANNEL_FILTER 1647 EXCLUDED_CHANNEL_FILTER) 1648 CREATEDATE_CLAUSE 1649 SEARCH_CLAUSE 1650 ORDER BY CreateAt DESC 1651 LIMIT 100` 1652 1653 inChannelClause, queryParams := s.buildSearchChannelFilterClause(params.InChannels, "InChannel", false, queryParams, channelsByName) 1654 searchQuery = strings.Replace(searchQuery, "IN_CHANNEL_FILTER", inChannelClause, 1) 1655 1656 excludedChannelClause, queryParams := s.buildSearchChannelFilterClause(params.ExcludedChannels, "ExcludedChannel", true, queryParams, channelsByName) 1657 searchQuery = strings.Replace(searchQuery, "EXCLUDED_CHANNEL_FILTER", excludedChannelClause, 1) 1658 1659 postFilterClause, queryParams := s.buildSearchPostFilterClause(params.FromUsers, params.ExcludedUsers, queryParams, userByUsername) 1660 searchQuery = strings.Replace(searchQuery, "POST_FILTER", postFilterClause, 1) 1661 1662 createDateFilterClause, queryParams := s.buildCreateDateFilterClause(params, queryParams) 1663 searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", createDateFilterClause, 1) 1664 1665 termMap := map[string]bool{} 1666 terms := params.Terms 1667 excludedTerms := params.ExcludedTerms 1668 1669 searchType := "Message" 1670 if params.IsHashtag { 1671 searchType = "Hashtags" 1672 for _, term := range strings.Split(terms, " ") { 1673 termMap[strings.ToUpper(term)] = true 1674 } 1675 } 1676 1677 // these chars have special meaning and can be treated as spaces 1678 for _, c := range specialSearchChar { 1679 terms = strings.Replace(terms, c, " ", -1) 1680 excludedTerms = strings.Replace(excludedTerms, c, " ", -1) 1681 } 1682 1683 if terms == "" && excludedTerms == "" { 1684 // we've already confirmed that we have a channel or user to search for 1685 searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) 1686 } else if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 1687 // Parse text for wildcards 1688 if wildcard, err := regexp.Compile(`\*($| )`); err == nil { 1689 terms = wildcard.ReplaceAllLiteralString(terms, ":* ") 1690 excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ") 1691 } 1692 1693 excludeClause := "" 1694 if excludedTerms != "" { 1695 excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")" 1696 } 1697 1698 if params.OrTerms { 1699 queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause 1700 } else if strings.HasPrefix(terms, `"`) && strings.HasSuffix(terms, `"`) { 1701 queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " <-> ") + ")" + excludeClause 1702 } else { 1703 queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause 1704 } 1705 1706 searchClause := fmt.Sprintf("AND to_tsvector('english', %s) @@ to_tsquery('english', :Terms)", searchType) 1707 searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) 1708 } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 1709 if searchType == "Message" { 1710 var err error 1711 terms, err = removeMysqlStopWordsFromTerms(terms) 1712 if err != nil { 1713 return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms") 1714 } 1715 1716 if terms == "" { 1717 return list, nil 1718 } 1719 } 1720 1721 searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType) 1722 searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) 1723 1724 excludeClause := "" 1725 if excludedTerms != "" { 1726 excludeClause = " -(" + excludedTerms + ")" 1727 } 1728 1729 if params.OrTerms { 1730 queryParams["Terms"] = terms + excludeClause 1731 } else { 1732 splitTerms := []string{} 1733 for _, t := range strings.Fields(terms) { 1734 splitTerms = append(splitTerms, "+"+t) 1735 } 1736 queryParams["Terms"] = strings.Join(splitTerms, " ") + excludeClause 1737 } 1738 } 1739 1740 _, err := s.GetSearchReplica().Select(&posts, searchQuery, queryParams) 1741 if err != nil { 1742 mlog.Warn("Query error searching posts.", mlog.Err(err)) 1743 // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. 1744 } else { 1745 for _, p := range posts { 1746 if searchType == "Hashtags" { 1747 exactMatch := false 1748 for _, tag := range strings.Split(p.Hashtags, " ") { 1749 if termMap[strings.ToUpper(tag)] { 1750 exactMatch = true 1751 break 1752 } 1753 } 1754 if !exactMatch { 1755 continue 1756 } 1757 } 1758 list.AddPost(p) 1759 list.AddOrder(p.Id) 1760 } 1761 } 1762 list.MakeNonNil() 1763 return list, nil 1764 } 1765 1766 func removeMysqlStopWordsFromTerms(terms string) (string, error) { 1767 stopWords := make([]string, len(searchlayer.MySQLStopWords)) 1768 copy(stopWords, searchlayer.MySQLStopWords) 1769 re, err := regexp.Compile(fmt.Sprintf(`^(%s)$`, strings.Join(stopWords, "|"))) 1770 if err != nil { 1771 return "", err 1772 } 1773 1774 newTerms := make([]string, 0) 1775 separatedTerms := strings.Fields(terms) 1776 for _, term := range separatedTerms { 1777 term = strings.TrimSpace(term) 1778 if term = re.ReplaceAllString(term, ""); term != "" { 1779 newTerms = append(newTerms, term) 1780 } 1781 } 1782 return strings.Join(newTerms, " "), nil 1783 } 1784 1785 func (s *SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) (model.AnalyticsRows, error) { 1786 query := 1787 `SELECT DISTINCT 1788 DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name, 1789 COUNT(DISTINCT Posts.UserId) AS Value 1790 FROM Posts` 1791 1792 if teamId != "" { 1793 query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = :TeamId AND" 1794 } else { 1795 query += " WHERE" 1796 } 1797 1798 query += ` Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime 1799 GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) 1800 ORDER BY Name DESC 1801 LIMIT 30` 1802 1803 if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 1804 query = 1805 `SELECT 1806 TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, COUNT(DISTINCT Posts.UserId) AS Value 1807 FROM Posts` 1808 1809 if teamId != "" { 1810 query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = :TeamId AND" 1811 } else { 1812 query += " WHERE" 1813 } 1814 1815 query += ` Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime 1816 GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) 1817 ORDER BY Name DESC 1818 LIMIT 30` 1819 } 1820 1821 end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday())) 1822 start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31))) 1823 1824 var rows model.AnalyticsRows 1825 _, err := s.GetReplica().Select( 1826 &rows, 1827 query, 1828 map[string]interface{}{"TeamId": teamId, "StartTime": start, "EndTime": end}) 1829 if err != nil { 1830 return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", teamId) 1831 } 1832 return rows, nil 1833 } 1834 1835 func (s *SqlPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) { 1836 1837 query := 1838 `SELECT 1839 DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name, 1840 COUNT(Posts.Id) AS Value 1841 FROM Posts` 1842 1843 if options.BotsOnly { 1844 query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid" 1845 } 1846 1847 if options.TeamId != "" { 1848 query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = :TeamId AND" 1849 } else { 1850 query += " WHERE" 1851 } 1852 1853 query += ` Posts.CreateAt <= :EndTime 1854 AND Posts.CreateAt >= :StartTime 1855 GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) 1856 ORDER BY Name DESC 1857 LIMIT 30` 1858 1859 if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 1860 query = 1861 `SELECT 1862 TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, Count(Posts.Id) AS Value 1863 FROM Posts` 1864 1865 if options.BotsOnly { 1866 query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid" 1867 } 1868 1869 if options.TeamId != "" { 1870 query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = :TeamId AND" 1871 } else { 1872 query += " WHERE" 1873 } 1874 1875 query += ` Posts.CreateAt <= :EndTime 1876 AND Posts.CreateAt >= :StartTime 1877 GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) 1878 ORDER BY Name DESC 1879 LIMIT 30` 1880 } 1881 1882 end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday())) 1883 start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31))) 1884 if options.YesterdayOnly { 1885 start = utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -1))) 1886 } 1887 1888 var rows model.AnalyticsRows 1889 _, err := s.GetReplica().Select( 1890 &rows, 1891 query, 1892 map[string]interface{}{"TeamId": options.TeamId, "StartTime": start, "EndTime": end}) 1893 if err != nil { 1894 return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", options.TeamId) 1895 } 1896 return rows, nil 1897 } 1898 1899 func (s *SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) { 1900 query := s.getQueryBuilder(). 1901 Select("COUNT(p.Id) AS Value"). 1902 From("Posts p") 1903 1904 if teamId != "" { 1905 query = query. 1906 Join("Channels c ON (c.Id = p.ChannelId)"). 1907 Where(sq.Eq{"c.TeamId": teamId}) 1908 } 1909 1910 if mustHaveFile { 1911 query = query.Where(sq.Or{sq.NotEq{"p.FileIds": "[]"}, sq.NotEq{"p.Filenames": "[]"}}) 1912 } 1913 1914 if mustHaveHashtag { 1915 query = query.Where(sq.NotEq{"p.Hashtags": ""}) 1916 } 1917 1918 queryString, args, err := query.ToSql() 1919 if err != nil { 1920 return 0, errors.Wrap(err, "post_tosql") 1921 } 1922 1923 v, err := s.GetReplica().SelectInt(queryString, args...) 1924 if err != nil { 1925 return 0, errors.Wrap(err, "failed to count Posts") 1926 } 1927 1928 return v, nil 1929 } 1930 1931 func (s *SqlPostStore) GetPostsCreatedAt(channelId string, time int64) ([]*model.Post, error) { 1932 query := `SELECT * FROM Posts WHERE CreateAt = :CreateAt AND ChannelId = :ChannelId` 1933 1934 var posts []*model.Post 1935 _, err := s.GetReplica().Select(&posts, query, map[string]interface{}{"CreateAt": time, "ChannelId": channelId}) 1936 1937 if err != nil { 1938 return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId) 1939 } 1940 return posts, nil 1941 } 1942 1943 func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) { 1944 keys, params := MapStringsToQueryParams(postIds, "Post") 1945 1946 query := `SELECT p.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id IN ` + keys + ` ORDER BY CreateAt DESC` 1947 1948 var posts []*model.Post 1949 _, err := s.GetReplica().Select(&posts, query, params) 1950 1951 if err != nil { 1952 return nil, errors.Wrap(err, "failed to find Posts") 1953 } 1954 return posts, nil 1955 } 1956 1957 func (s *SqlPostStore) GetPostsBatchForIndexing(startTime int64, endTime int64, limit int) ([]*model.PostForIndexing, error) { 1958 var posts []*model.PostForIndexing 1959 _, err := s.GetSearchReplica().Select(&posts, 1960 `SELECT 1961 PostsQuery.*, Channels.TeamId, ParentPosts.CreateAt ParentCreateAt 1962 FROM ( 1963 SELECT 1964 * 1965 FROM 1966 Posts 1967 WHERE 1968 Posts.CreateAt >= :StartTime 1969 AND 1970 Posts.CreateAt < :EndTime 1971 ORDER BY 1972 CreateAt ASC 1973 LIMIT 1974 1000 1975 ) 1976 AS 1977 PostsQuery 1978 LEFT JOIN 1979 Channels 1980 ON 1981 PostsQuery.ChannelId = Channels.Id 1982 LEFT JOIN 1983 Posts ParentPosts 1984 ON 1985 PostsQuery.RootId = ParentPosts.Id`, 1986 map[string]interface{}{"StartTime": startTime, "EndTime": endTime, "NumPosts": limit}) 1987 1988 if err != nil { 1989 return nil, errors.Wrap(err, "failed to find Posts") 1990 } 1991 return posts, nil 1992 } 1993 1994 // PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by 1995 // the global or a granular retention policy. 1996 // See `genericPermanentDeleteBatchForRetentionPolicies` for details. 1997 func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { 1998 builder := s.getQueryBuilder(). 1999 Select("Posts.Id"). 2000 From("Posts") 2001 return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{ 2002 BaseBuilder: builder, 2003 Table: "Posts", 2004 TimeColumn: "CreateAt", 2005 PrimaryKeys: []string{"Id"}, 2006 ChannelIDTable: "Posts", 2007 NowMillis: now, 2008 GlobalPolicyEndTime: globalPolicyEndTime, 2009 Limit: limit, 2010 }, s.SqlStore, cursor) 2011 } 2012 2013 // DeleteOrphanedRows removes entries from Posts when a corresponding channel no longer exists. 2014 func (s *SqlPostStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { 2015 // We need the extra level of nesting to deal with MySQL's locking 2016 const query = ` 2017 DELETE FROM Posts WHERE Id IN ( 2018 SELECT * FROM ( 2019 SELECT Posts.Id FROM Posts 2020 LEFT JOIN Channels ON Posts.ChannelId = Channels.Id 2021 WHERE Channels.Id IS NULL 2022 LIMIT :Limit 2023 ) AS A 2024 )` 2025 props := map[string]interface{}{"Limit": limit} 2026 result, err := s.GetMaster().Exec(query, props) 2027 if err != nil { 2028 return 2029 } 2030 deleted, err = result.RowsAffected() 2031 return 2032 } 2033 2034 func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) { 2035 var query string 2036 if s.DriverName() == "postgres" { 2037 query = "DELETE from Posts WHERE Id = any (array (SELECT Id FROM Posts WHERE CreateAt < :EndTime LIMIT :Limit))" 2038 } else { 2039 query = "DELETE from Posts WHERE CreateAt < :EndTime LIMIT :Limit" 2040 } 2041 2042 sqlResult, err := s.GetMaster().Exec(query, map[string]interface{}{"EndTime": endTime, "Limit": limit}) 2043 if err != nil { 2044 return 0, errors.Wrap(err, "failed to delete Posts") 2045 } 2046 2047 rowsAffected, err := sqlResult.RowsAffected() 2048 if err != nil { 2049 return 0, errors.Wrap(err, "failed to delete Posts") 2050 } 2051 return rowsAffected, nil 2052 } 2053 2054 func (s *SqlPostStore) GetOldest() (*model.Post, error) { 2055 var post model.Post 2056 err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts ORDER BY CreateAt LIMIT 1") 2057 if err != nil { 2058 if err == sql.ErrNoRows { 2059 return nil, store.NewErrNotFound("Post", "none") 2060 } 2061 2062 return nil, errors.Wrap(err, "failed to get oldest Post") 2063 } 2064 2065 return &post, nil 2066 } 2067 2068 func (s *SqlPostStore) determineMaxPostSize() int { 2069 var maxPostSizeBytes int32 2070 2071 if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { 2072 // The Post.Message column in Postgres has historically been VARCHAR(4000), but 2073 // may be manually enlarged to support longer posts. 2074 if err := s.GetReplica().SelectOne(&maxPostSizeBytes, ` 2075 SELECT 2076 COALESCE(character_maximum_length, 0) 2077 FROM 2078 information_schema.columns 2079 WHERE 2080 table_name = 'posts' 2081 AND column_name = 'message' 2082 `); err != nil { 2083 mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err)) 2084 } 2085 } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { 2086 // The Post.Message column in MySQL has historically been TEXT, with a maximum 2087 // limit of 65535. 2088 if err := s.GetReplica().SelectOne(&maxPostSizeBytes, ` 2089 SELECT 2090 COALESCE(CHARACTER_MAXIMUM_LENGTH, 0) 2091 FROM 2092 INFORMATION_SCHEMA.COLUMNS 2093 WHERE 2094 table_schema = DATABASE() 2095 AND table_name = 'Posts' 2096 AND column_name = 'Message' 2097 LIMIT 0, 1 2098 `); err != nil { 2099 mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err)) 2100 } 2101 } else { 2102 mlog.Warn("No implementation found to determine the maximum supported post size") 2103 } 2104 2105 // Assume a worst-case representation of four bytes per rune. 2106 maxPostSize := int(maxPostSizeBytes) / 4 2107 2108 // To maintain backwards compatibility, don't yield a maximum post 2109 // size smaller than the previous limit, even though it wasn't 2110 // actually possible to store 4000 runes in all cases. 2111 if maxPostSize < model.POST_MESSAGE_MAX_RUNES_V1 { 2112 maxPostSize = model.POST_MESSAGE_MAX_RUNES_V1 2113 } 2114 2115 mlog.Info("Post.Message has size restrictions", mlog.Int("max_characters", maxPostSize), mlog.Int32("max_bytes", maxPostSizeBytes)) 2116 2117 return maxPostSize 2118 } 2119 2120 // GetMaxPostSize returns the maximum number of runes that may be stored in a post. 2121 func (s *SqlPostStore) GetMaxPostSize() int { 2122 s.maxPostSizeOnce.Do(func() { 2123 s.maxPostSizeCached = s.determineMaxPostSize() 2124 }) 2125 return s.maxPostSizeCached 2126 } 2127 2128 func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*model.PostForExport, error) { 2129 for { 2130 var rootIds []string 2131 _, err := s.GetReplica().Select(&rootIds, 2132 `SELECT 2133 Id 2134 FROM 2135 Posts 2136 WHERE 2137 Id > :AfterId 2138 AND RootId = '' 2139 AND DeleteAt = 0 2140 ORDER BY Id 2141 LIMIT :Limit`, 2142 map[string]interface{}{"Limit": limit, "AfterId": afterId}) 2143 if err != nil { 2144 return nil, errors.Wrap(err, "failed to find Posts") 2145 } 2146 2147 var postsForExport []*model.PostForExport 2148 if len(rootIds) == 0 { 2149 return postsForExport, nil 2150 } 2151 2152 keys, params := MapStringsToQueryParams(rootIds, "PostId") 2153 _, err = s.GetSearchReplica().Select(&postsForExport, ` 2154 SELECT 2155 p1.*, 2156 Users.Username as Username, 2157 Teams.Name as TeamName, 2158 Channels.Name as ChannelName 2159 FROM 2160 (Select * FROM Posts WHERE Id IN `+keys+`) p1 2161 INNER JOIN 2162 Channels ON p1.ChannelId = Channels.Id 2163 INNER JOIN 2164 Teams ON Channels.TeamId = Teams.Id 2165 INNER JOIN 2166 Users ON p1.UserId = Users.Id 2167 WHERE 2168 Channels.DeleteAt = 0 2169 AND Teams.DeleteAt = 0 2170 ORDER BY 2171 p1.Id`, 2172 params) 2173 if err != nil { 2174 return nil, errors.Wrap(err, "failed to find Posts") 2175 } 2176 2177 if len(postsForExport) == 0 { 2178 // All of the posts were in channels or teams that were deleted. 2179 // Update the afterId and try again. 2180 afterId = rootIds[len(rootIds)-1] 2181 continue 2182 } 2183 2184 return postsForExport, nil 2185 } 2186 } 2187 2188 func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) { 2189 var posts []*model.ReplyForExport 2190 _, err := s.GetSearchReplica().Select(&posts, ` 2191 SELECT 2192 Posts.*, 2193 Users.Username as Username 2194 FROM 2195 Posts 2196 INNER JOIN 2197 Users ON Posts.UserId = Users.Id 2198 WHERE 2199 Posts.RootId = :RootId 2200 AND Posts.DeleteAt = 0 2201 ORDER BY 2202 Posts.Id`, 2203 map[string]interface{}{"RootId": rootId}) 2204 2205 if err != nil { 2206 return nil, errors.Wrap(err, "failed to find Posts") 2207 } 2208 2209 return posts, nil 2210 } 2211 2212 func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string) ([]*model.DirectPostForExport, error) { 2213 query := s.getQueryBuilder(). 2214 Select("p.*", "Users.Username as User"). 2215 From("Posts p"). 2216 Join("Channels ON p.ChannelId = Channels.Id"). 2217 Join("Users ON p.UserId = Users.Id"). 2218 Where(sq.And{ 2219 sq.Gt{"p.Id": afterId}, 2220 sq.Eq{"p.ParentId": string("")}, 2221 sq.Eq{"p.DeleteAt": int(0)}, 2222 sq.Eq{"Channels.DeleteAt": int(0)}, 2223 sq.Eq{"Users.DeleteAt": int(0)}, 2224 sq.Eq{"Channels.Type": []string{"D", "G"}}, 2225 }). 2226 OrderBy("p.Id"). 2227 Limit(uint64(limit)) 2228 2229 queryString, args, err := query.ToSql() 2230 if err != nil { 2231 return nil, errors.Wrap(err, "post_tosql") 2232 } 2233 2234 var posts []*model.DirectPostForExport 2235 if _, err = s.GetReplica().Select(&posts, queryString, args...); err != nil { 2236 return nil, errors.Wrap(err, "failed to find Posts") 2237 } 2238 var channelIds []string 2239 for _, post := range posts { 2240 channelIds = append(channelIds, post.ChannelId) 2241 } 2242 query = s.getQueryBuilder(). 2243 Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest"). 2244 From("ChannelMembers cm"). 2245 Join("Users u ON ( u.Id = cm.UserId )"). 2246 Where(sq.Eq{ 2247 "cm.ChannelId": channelIds, 2248 }) 2249 2250 queryString, args, err = query.ToSql() 2251 if err != nil { 2252 return nil, errors.Wrap(err, "post_tosql") 2253 } 2254 2255 var channelMembers []*model.ChannelMemberForExport 2256 if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil { 2257 return nil, errors.Wrap(err, "failed to find ChannelMembers") 2258 } 2259 2260 // Build a map of channels and their posts 2261 postsChannelMap := make(map[string][]*model.DirectPostForExport) 2262 for _, post := range posts { 2263 post.ChannelMembers = &[]string{} 2264 postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post) 2265 } 2266 2267 // Build a map of channels and their members 2268 channelMembersMap := make(map[string][]string) 2269 for _, member := range channelMembers { 2270 channelMembersMap[member.ChannelId] = append(channelMembersMap[member.ChannelId], member.Username) 2271 } 2272 2273 // Populate each post ChannelMembers extracting it from the channelMembersMap 2274 for channelId := range channelMembersMap { 2275 for _, post := range postsChannelMap[channelId] { 2276 *post.ChannelMembers = channelMembersMap[channelId] 2277 } 2278 } 2279 return posts, nil 2280 } 2281 2282 //nolint:unparam 2283 func (s *SqlPostStore) SearchPostsInTeamForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) { 2284 // Since we don't support paging for DB search, we just return nothing for later pages 2285 if page > 0 { 2286 return model.MakePostSearchResults(model.NewPostList(), nil), nil 2287 } 2288 2289 if err := model.IsSearchParamsListValid(paramsList); err != nil { 2290 return nil, err 2291 } 2292 2293 var wg sync.WaitGroup 2294 2295 pchan := make(chan store.StoreResult, len(paramsList)) 2296 2297 for _, params := range paramsList { 2298 // remove any unquoted term that contains only non-alphanumeric chars 2299 // ex: abcd "**" && abc >> abcd "**" abc 2300 params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ") 2301 2302 wg.Add(1) 2303 2304 go func(params *model.SearchParams) { 2305 defer wg.Done() 2306 postList, err := s.search(teamId, userId, params, false, false) 2307 pchan <- store.StoreResult{Data: postList, NErr: err} 2308 }(params) 2309 } 2310 2311 wg.Wait() 2312 close(pchan) 2313 2314 posts := model.NewPostList() 2315 2316 for result := range pchan { 2317 if result.NErr != nil { 2318 return nil, result.NErr 2319 } 2320 data := result.Data.(*model.PostList) 2321 posts.Extend(data) 2322 } 2323 2324 posts.SortByCreateAt() 2325 2326 return model.MakePostSearchResults(posts, nil), nil 2327 } 2328 2329 func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) { 2330 query := s.getQueryBuilder().Select("MIN(min_createat) min_createat"). 2331 Suffix(`FROM ( 2332 (SELECT MIN(createat) min_createat FROM Posts) 2333 UNION 2334 (SELECT MIN(createat) min_createat FROM Users) 2335 UNION 2336 (SELECT MIN(createat) min_createat FROM Channels) 2337 ) entities`) 2338 queryString, _, err := query.ToSql() 2339 if err != nil { 2340 return -1, errors.Wrap(err, "post_tosql") 2341 } 2342 row := s.GetReplica().Db.QueryRow(queryString) 2343 var oldest int64 2344 if err := row.Scan(&oldest); err != nil { 2345 return -1, errors.Wrap(err, "unable to scan oldest entity creation time") 2346 } 2347 return oldest, nil 2348 } 2349 2350 func (s *SqlPostStore) cleanupThreads(postId, rootId string, permanent bool) error { 2351 if permanent { 2352 if _, err := s.GetMaster().Exec("DELETE FROM Threads WHERE PostId = :Id", map[string]interface{}{"Id": postId}); err != nil { 2353 return errors.Wrap(err, "failed to delete Threads") 2354 } 2355 if _, err := s.GetMaster().Exec("DELETE FROM ThreadMemberships WHERE PostId = :Id", map[string]interface{}{"Id": postId}); err != nil { 2356 return errors.Wrap(err, "failed to delete ThreadMemberships") 2357 } 2358 return nil 2359 } 2360 if rootId != "" { 2361 thread, err := s.Thread().Get(rootId) 2362 if err != nil { 2363 var nfErr *store.ErrNotFound 2364 if !errors.As(err, &nfErr) { 2365 return errors.Wrap(err, "failed to get a thread") 2366 } 2367 } 2368 if thread != nil { 2369 thread.ReplyCount -= 1 2370 if _, err = s.Thread().Update(thread); err != nil { 2371 return errors.Wrap(err, "failed to update thread") 2372 } 2373 } 2374 } 2375 return nil 2376 } 2377 2378 func (s *SqlPostStore) updateThreadsFromPosts(transaction *gorp.Transaction, posts []*model.Post) error { 2379 postsByRoot := map[string][]*model.Post{} 2380 var rootIds []string 2381 for _, post := range posts { 2382 // skip if post is not a part of a thread 2383 if post.RootId == "" { 2384 continue 2385 } 2386 rootIds = append(rootIds, post.RootId) 2387 postsByRoot[post.RootId] = append(postsByRoot[post.RootId], post) 2388 } 2389 if len(rootIds) == 0 { 2390 return nil 2391 } 2392 threadsByRootsSql, threadsByRootsArgs, _ := s.getQueryBuilder().Select("*").From("Threads").Where(sq.Eq{"PostId": rootIds}).ToSql() 2393 var threadsByRoots []*model.Thread 2394 if _, err := transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...); err != nil { 2395 return err 2396 } 2397 2398 threadByRoot := map[string]*model.Thread{} 2399 for _, thread := range threadsByRoots { 2400 threadByRoot[thread.PostId] = thread 2401 } 2402 2403 for rootId, posts := range postsByRoot { 2404 if thread, found := threadByRoot[rootId]; !found { 2405 // calculate participants 2406 var participants model.StringArray 2407 if _, err := transaction.Select(&participants, "SELECT DISTINCT UserId FROM Posts WHERE RootId=:RootId OR Id=:RootId", map[string]interface{}{"RootId": rootId}); err != nil { 2408 return err 2409 } 2410 // calculate reply count 2411 count, err := transaction.SelectInt("SELECT COUNT(Id) FROM Posts WHERE RootId=:RootId And DeleteAt=0", map[string]interface{}{"RootId": rootId}) 2412 if err != nil { 2413 return err 2414 } 2415 // calculate last reply at 2416 lastReplyAt, err := transaction.SelectInt("SELECT COALESCE(MAX(Posts.CreateAt), 0) FROM Posts WHERE RootID=:RootId and DeleteAt=0", map[string]interface{}{"RootId": rootId}) 2417 if err != nil { 2418 return err 2419 } 2420 // no metadata entry, create one 2421 if err := transaction.Insert(&model.Thread{ 2422 PostId: rootId, 2423 ChannelId: posts[0].ChannelId, 2424 ReplyCount: count, 2425 LastReplyAt: lastReplyAt, 2426 Participants: participants, 2427 }); err != nil { 2428 return err 2429 } 2430 } else { 2431 // metadata exists, update it 2432 for _, post := range posts { 2433 thread.ReplyCount += 1 2434 if !thread.Participants.Contains(post.UserId) { 2435 thread.Participants = append(thread.Participants, post.UserId) 2436 } 2437 if post.CreateAt > thread.LastReplyAt { 2438 thread.LastReplyAt = post.CreateAt 2439 } 2440 } 2441 if _, err := transaction.Update(thread); err != nil { 2442 return err 2443 } 2444 } 2445 } 2446 return nil 2447 }