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