MM-59966 - Compliance Export overhaul - feature branch (#29789)

* [MM-59089] Add a compliance export constant (#27919)

* add a useful constant

* i18n

* another constant

* another i18n

* [MM-60422] Add GetChannelsWithActivityDuring (#28301)

* modify GetUsersInChannelDuring to accept a slice of channelIds

* add GetChannelsWithActivityDuring

* add compliance export progress message; remove unused custom status

* linting

* tests running too fast

* add batch size config settings

* add store tests

* linting

* empty commit

* i18n changes

* fix i18n ordering

* MM-60570 - Server-side changes consolidating the export CLI with server/ent code (#28640)

* add an i18n field; add the CLI's export directory

* int64 -> int

* Add UntilUpdateAt for MessageExport and AnalyticsPostCount

to merge

* remove now-unused i18n strings

* add TranslationsPreInitFromBuffer to allow CLI to use i18n

* use GetBuilder to simplify; rename TranslationsPreInitFromFileBytes

* [MM-59089] Improve compliance export timings (#1733 - Enterprise repo)

* MM-60422 - Performance and logic fixes for Compliance Exports (#1757 - Enterprise repo)

* MM-60570 - Enterprise-side changes consolidating the export CLI with server/ent code (#1769 - Enterprise repo)

* merge conflicts; missed file from ent branch

* MM-61038 - Add an option to sqlstore.New (#28702)

remove useless comment

add test

add an option to sqlstore.New

* MM-60976: Remove RunExport command from Mattermost binary (#28805)

* remove RunExport command from mattermost binary

* remove the code it was calling

* fix i18n

* remove test (was only testing license, not functionality)

* empty commit

* fix flaky GetChannelsWithActivityDuring test

* MM-60063: Dedicated Export Filestore fix, redo of #1772 (enterprise) (#28803)

* redo filestore fix #1772 (enterprise repo) on top of MM-59966 feature

* add new e2e tests for export filestore

* golint

* ok, note to self: shadowing bad, actually (when there's a defer)

* empty commit

* MM-61137 - Message export: Support 7.8.11 era dbs (#28824)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* blank commit

* MM-60974 - Message Export: Add performance metrics (#28836)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* cleanup unneeded named returns

* blank commit

* MM-60975 - Message export: Add startTime and endTime to export folder name (#28840)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* output startTime and endTime in export folder

* empty commit

* merge conflict

* MM-60978 - Message export: Improve xml fields; fix delete semantics (#28873)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* output startTime and endTime in export folder

* empty commit

* add xml fields, omit when empty, tests

* fix delete semantics; test (and test for update semantics)

* clarify comments

* simplify edited post detection, now there's no edge case.

* add some spacing to help fast running tests

* merge conflicts/updates needed for new deleted post semantics

* linting; fixing tests from upstream merge

* use SafeDereference

* linting

* stronger typing; better wrapped errors; better formatting

* blank commit

* goimports formatting

* fix merge mistake

* minor fixes due to changes in master

* MM-61755 - Simplifying and Support reporting to the db from the CLI (#29281)

* finally clean up JobData struct and stringMap; prep for CLI using db

* and now simplify using StringMapToJobDataWithZeroValues

* remove unused fn

* create JobDataExported; clean up errors

* MM-60176 - Message Export: Global relay cleanup (#29168)

* move global relay logic into global_relay_export

* blank commit

* blank commit

* improve errors

* MM-60693 - Refactor CSV to use same codepath as Actiance (#29191)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* MM-61696 - Refactor GlobalRelay to use same codepath as Actiance (#29225)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* remove newly unneeded function and its test. goodbye.

* refactor GetPostAttachments for csv + global relay to share

* refactor global_relay_export and fix tests (no changes to output)

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* remove unneeded nil check

* PR comments

* MM-61715 - Generalize e2e to all export types 🤖  (#29369)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* remove newly unneeded function and its test. goodbye.

* refactor GetPostAttachments for csv + global relay to share

* refactor global_relay_export and fix tests (no changes to output)

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* remove unneeded nil check

* PR comments

* refactor isDeletedMsg for all export types

* fix start and endtime, nasty csv createAt bug; bring closer to Actiance

* align unit tests with new logic (e.g. starttime / endtime)

* refactor a TimestampConvert fn for code + tests

* bug: pass templates to global relay (hurray for e2e tests, otherwise...)

* add global relay zip to allowed list (only for tests)

* test helpers

* new templates for e2e tests

* e2e tests... phew.

* linting

* merge conflicts

* unexport PostToRow; add test helper marker

* cleanup, shortening, thanks to PR comments

* MM-61972 - Generalize export data path - Actiance (#29399)

* extract and generalize the export data generation functions

* finish moving test (bc of previous extraction)

* lift a function from common -> shared (to break an import cycle)

* actiance now takes general export data, processes it into actiance data

* bring tests in line with correct sorting rules (upadateAt, messageId)

* fixups, PR comments

* turn strings.Repeat into a more descriptive const

amended: one letter fix; bad rebase

* MM-62009 - e2e clock heisenbug (#29434)

* consolidate assertions; output debuggable diffs (keeping for future)

* refactor test output generator to generators file

* waitUntilZeroPosts + pass through until to job = fix all clock issues

* simplify messages to model.NewId(); remove unneeded waitUntilZeroPosts

* model.NewId() -> storetest.NewTestID()

* MM-61980 - Generalize export data path - CSV (#29482)

* simple refactoring

* increase sleep times for (very) rare test failures

* add extra information to the generic export for CSV

* adj Actiance to handle new generic export (no difference in its output)

* no longer need mergePosts (yay), move getJoinLeavePosts for everyone

* adjust tests for new csv semantics (detailed in summary)

* and need to add the new exported data to the export_data_tests

* rearrange csv writing to happen after data export (more logical)

* linting

* remove debug statements

* figured out what was wrong with global relay e2e test 3; solid now

* PR comments

* MM-61718 - Generalize export data path - Global Relay (#29508)

* move global relay over to using the generalized export data

* performance pass -- not much can be done

* Update server/enterprise/message_export/global_relay_export/global_relay_export.go

Co-authored-by: Claudio Costa <cstcld91@gmail.com>

---------

Co-authored-by: Claudio Costa <cstcld91@gmail.com>

* MM-62058 - Align CSV with Actiance (#29551)

* refactoring actiance files and var names for clarity

* bug found in exported attachments (we used to miss some start/ends)

* changes needed for actiance due to new generic exports

* bringing CSV up to actiance standards

* fixing global relay b/c of new semantics (adding a note on an edge case)

* aligning e2e tests, adding comments to clarify what is expected/tested

* necessary changes; 1 more test for added functionality (ignoreDeleted)

* comment style

* MM-62059 - Align Global Relay with Actiance/CSV; many fixes (#29665)

* core logic changes to general export_data and the specific export paths

* unit tests and e2e tests, covering all new edge cases and all logic

* linting

* better var naming, const value, and cleaning up functions calls

* MM-62436 - Temporarily skip cypress tests that require download link (#29772)

---------

Co-authored-by: Claudio Costa <cstcld91@gmail.com>
Этот коммит содержится в:
Christopher Poile
2025-01-10 16:56:02 -05:00
коммит произвёл GitHub
родитель 96195f1bd7
Коммит aba4434dab
103 изменённых файлов: 14696 добавлений и 3091 удалений

Просмотреть файл

@@ -298,7 +298,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testi
channel, err := th.App.Srv().Store().Channel().GetByName(th.BasicTeam.Id, "town-square", true)
require.NoError(t, err)
townSquareChannelID := channel.Id
users, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, townSquareChannelID)
users, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{townSquareChannelID})
require.NoError(t, nErr)
initialNumTownSquareUsers := len(users)
@@ -308,7 +308,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testi
require.Nil(t, appErr)
// there should be a ChannelMemberHistory record for the user
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, townSquareChannelID)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{townSquareChannelID})
require.NoError(t, nErr)
assert.Len(t, histories, initialNumTownSquareUsers+1)
@@ -330,7 +330,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing
channel, err := th.App.Srv().Store().Channel().GetByName(th.BasicTeam.Id, "off-topic", true)
require.NoError(t, err)
offTopicChannelId := channel.Id
users, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, offTopicChannelId)
users, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{offTopicChannelId})
require.NoError(t, nErr)
initialNumTownSquareUsers := len(users)
@@ -340,7 +340,7 @@ func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing
require.Nil(t, appError)
// there should be a ChannelMemberHistory record for the user
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, offTopicChannelId)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{offTopicChannelId})
require.NoError(t, nErr)
assert.Len(t, histories, initialNumTownSquareUsers+1)
@@ -418,7 +418,7 @@ func TestCreateChannelPublicCreatesChannelMemberHistoryRecord(t *testing.T) {
publicChannel := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
// there should be a ChannelMemberHistory record for the user
histories, err := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, publicChannel.Id)
histories, err := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{publicChannel.Id})
require.NoError(t, err)
assert.Len(t, histories, 1)
assert.Equal(t, th.BasicUser.Id, histories[0].UserId)
@@ -433,7 +433,7 @@ func TestCreateChannelPrivateCreatesChannelMemberHistoryRecord(t *testing.T) {
privateChannel := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypePrivate)
// there should be a ChannelMemberHistory record for the user
histories, err := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, privateChannel.Id)
histories, err := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{privateChannel.Id})
require.NoError(t, err)
assert.Len(t, histories, 1)
assert.Equal(t, th.BasicUser.Id, histories[0].UserId)
@@ -557,7 +557,7 @@ func TestCreateGroupChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
channel, appErr := th.App.CreateGroupChannel(th.Context, groupUserIds, th.BasicUser.Id)
require.Nil(t, appErr, "Failed to create group channel.")
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{channel.Id})
require.NoError(t, nErr)
assert.Len(t, histories, 3)
@@ -582,7 +582,7 @@ func TestCreateDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
channel, appErr := th.App.GetOrCreateDirectChannel(th.Context, user1.Id, user2.Id)
require.Nil(t, appErr, "Failed to create direct channel.")
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{channel.Id})
require.NoError(t, nErr)
assert.Len(t, histories, 2)
@@ -610,7 +610,7 @@ func TestGetDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
require.Nil(t, appErr, "Failed to create direct channel.")
// there should be a ChannelMemberHistory record for both users
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{channel.Id})
require.NoError(t, nErr)
assert.Len(t, histories, 2)
@@ -645,7 +645,7 @@ func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
require.Nil(t, appErr, "Failed to add user to channel.")
// there should be a ChannelMemberHistory record for the user
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{channel.Id})
require.NoError(t, nErr)
assert.Len(t, histories, 2)
channelMemberHistoryUserIds := make([]string, 0)
@@ -656,6 +656,88 @@ func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
assert.Equal(t, groupUserIds, channelMemberHistoryUserIds)
}
func TestUsersAndPostsCreateActivityInChannel(t *testing.T) {
th := Setup(t).InitBasic().DeleteBots()
defer th.TearDown()
user := th.CreateUser()
_, err := th.App.AddTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err, "Failed to add user to team.")
user3 := th.CreateUser()
_, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, user3.Id)
require.Nil(t, err, "Failed to add user to team.")
user4 := th.CreateUser()
_, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, user4.Id)
require.Nil(t, err, "Failed to add user to team.")
channel1 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
channel2 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
channel3 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
channel4 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
channel5 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
channel6 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
// user3 is already in channel3
_, err = th.App.AddUserToChannel(th.Context, user3, channel3, false)
require.Nil(t, err, "Failed to add user to channel.")
// user4 is already in channel4 (for the second part of the test)
_, err = th.App.AddUserToChannel(th.Context, user4, channel4, false)
require.Nil(t, err, "Failed to add user to channel.")
// make sure we don't catch earlier posts
time.Sleep(10 * time.Millisecond)
testStart := model.GetMillis()
// Test: previous activity (user3 and 4's adds) aren't showing up:
channelIds, nErr := th.App.Srv().Store().ChannelMemberHistory().GetChannelsWithActivityDuring(testStart, testStart+10000)
require.NoError(t, nErr)
assert.Len(t, channelIds, 0)
// Posts, adds, and leaves should create activity
post := &model.Post{
ChannelId: channel1.Id,
Message: "root post",
UserId: th.BasicUser.Id,
}
_, err = th.App.CreatePost(th.Context, post, channel1, model.CreatePostFlags{})
require.Nil(t, err, "Failed to create post.")
_, err = th.App.AddUserToChannel(th.Context, user, channel2, false)
require.Nil(t, err, "Failed to add user to channel.")
err = th.App.RemoveUserFromChannel(th.Context, user3.Id, user3.Id, channel3)
require.Nil(t, err, "Failed to add user to channel.")
// Test: there should be a ChannelMemberHistory record for the users and the post
channelIds, nErr = th.App.Srv().Store().ChannelMemberHistory().GetChannelsWithActivityDuring(testStart, model.GetMillis())
require.NoError(t, nErr)
assert.Len(t, channelIds, 3)
assert.ElementsMatch(t, []string{channel1.Id, channel2.Id, channel3.Id}, channelIds)
testEnd := model.GetMillis()
// In case the tests are running very fast:
time.Sleep(10 * time.Millisecond)
// Now, we do not find activity for new posts, leaves, or adds after the test is over
post2 := &model.Post{
ChannelId: channel5.Id,
Message: "root post",
UserId: th.BasicUser.Id,
}
err = th.App.RemoveUserFromChannel(th.Context, user4.Id, user4.Id, channel4)
require.Nil(t, err, "Failed to create post.")
_, err = th.App.CreatePost(th.Context, post2, channel5, model.CreatePostFlags{})
require.Nil(t, err, "Failed to create post.")
_, err = th.App.AddUserToChannel(th.Context, user, channel6, false)
require.Nil(t, err, "Failed to add user to channel.")
// Test: we get the same three channels as before, not channels 4, 5, 6 which have activity after testEnd
channelIds, nErr = th.App.Srv().Store().ChannelMemberHistory().GetChannelsWithActivityDuring(testStart, testEnd)
require.NoError(t, nErr)
assert.Len(t, channelIds, 3)
assert.ElementsMatch(t, []string{channel1.Id, channel2.Id, channel3.Id}, channelIds)
}
func TestLeaveDefaultChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
@@ -811,7 +893,7 @@ func TestAddChannelMemberNoUserRequestor(t *testing.T) {
require.Nil(t, appErr, "Failed to add user to channel.")
// there should be a ChannelMemberHistory record for the user
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)
histories, nErr := th.App.Srv().Store().ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, []string{channel.Id})
require.NoError(t, nErr)
assert.Len(t, histories, 2)
channelMemberHistoryUserIds := make([]string, 0)

Просмотреть файл

@@ -103,7 +103,7 @@ func (a *App) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.
backend, err = filestore.NewFileBackend(filestore.NewFileBackendSettingsFromConfig(cfg, complianceEnabled, insecure != nil && *insecure))
}
if err != nil {
return model.NewAppError("FileBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return model.NewAppError("FileAttachmentBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
nErr := backend.TestConnection()
if nErr != nil {

Просмотреть файл

@@ -2933,7 +2933,25 @@ func (s *OpenTracingLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
func (s *OpenTracingLayerChannelMemberHistoryStore) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetChannelsWithActivityDuring")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.GetChannelsWithActivityDuring(startTime, endTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetUsersInChannelDuring")
s.Root.Store.SetContext(newCtx)

Просмотреть файл

@@ -3270,7 +3270,28 @@ func (s *RetryLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string
}
func (s *RetryLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
func (s *RetryLayerChannelMemberHistoryStore) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.GetChannelsWithActivityDuring(startTime, endTime)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error) {
tries := 0
for {

Просмотреть файл

@@ -65,27 +65,78 @@ func (s SqlChannelMemberHistoryStore) LogLeaveEvent(userId string, channelId str
return nil
}
func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
func (s SqlChannelMemberHistoryStore) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) {
// ChannelMemberHistory has been in production for long enough that we are assuming the export period
// starts after the ChannelMemberHistory table was first introduced
subqueryPosts := s.getSubQueryBuilder().
Select("p.ChannelId").
Distinct().
From("Posts AS p").
Where(
sq.And{
sq.GtOrEq{"p.UpdateAt": startTime},
sq.LtOrEq{"p.UpdateAt": endTime},
sq.NotLike{"p.Type": "system_%"},
})
subqueryCMH := s.getSubQueryBuilder().
Select("cmh.ChannelId").
Distinct().
From("ChannelMemberHistory AS cmh").
Where(
sq.Or{
sq.And{
sq.GtOrEq{"cmh.JoinTime": startTime},
sq.LtOrEq{"cmh.JoinTime": endTime},
},
sq.And{
sq.GtOrEq{"cmh.LeaveTime": startTime},
sq.LtOrEq{"cmh.LeaveTime": endTime},
},
})
unionExpr, args, err := sq.Expr("(? UNION ?) AS cm", subqueryPosts, subqueryCMH).ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetChannelsWithActivityDuring unionExpr to sql")
}
// no bound args in this expression
query, _, err := s.getQueryBuilder().
Select("*").
From(unionExpr).ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetChannelsWithActivityDuring query to sql")
}
channelIds := make([]string, 0)
if err := s.GetReplica().Select(&channelIds, query, args...); err != nil {
return nil, err
}
return channelIds, nil
}
func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelIds []string) ([]*model.ChannelMemberHistoryResult, error) {
useChannelMemberHistory, err := s.hasDataAtOrBefore(startTime)
if err != nil {
return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%v", startTime, endTime, channelIds)
}
if useChannelMemberHistory {
// the export period starts after the ChannelMemberHistory table was first introduced, so we can use the
// data from it for our export
channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelId)
channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelIds)
if err2 != nil {
return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%v", startTime, endTime, channelIds)
}
return channelMemberHistories, nil
}
// the export period starts before the ChannelMemberHistory table was introduced, so we need to fake the
// data by assuming that anybody who has ever joined the channel in question was present during the export period.
// this may not always be true, but it's better than saying that somebody wasn't there when they were
channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelId)
channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelIds)
if err != nil {
return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%v", startTime, endTime, channelIds)
}
return channelMemberHistories, nil
}
@@ -108,14 +159,14 @@ func (s SqlChannelMemberHistoryStore) hasDataAtOrBefore(time int64) (bool, error
return false, nil
}
func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelIds []string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`cmh.*, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
From("ChannelMemberHistory cmh").
Join("Users u ON cmh.UserId = u.Id").
LeftJoin("Bots ON Bots.UserId = u.Id").
Where(sq.And{
sq.Eq{"cmh.ChannelId": channelId},
sq.Eq{"cmh.ChannelId": channelIds},
sq.LtOrEq{"cmh.JoinTime": endTime},
sq.Or{
sq.Eq{"cmh.LeaveTime": nil},
@@ -134,14 +185,14 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime
return histories, nil
}
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelIds []string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`ch.ChannelId, ch.UserId, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
Distinct().
From("ChannelMembers ch").
Join("Users u ON ch.UserId = u.id").
LeftJoin("Bots ON Bots.UserId = u.id").
Where(sq.Eq{"ch.ChannelId": channelId}).ToSql()
Where(sq.Eq{"ch.ChannelId": channelIds}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}

Просмотреть файл

@@ -286,24 +286,25 @@ func (s SqlComplianceStore) MessageExport(c request.CTX, cursor model.MessageExp
return nil, cursor, errors.Wrap(caseErr, "unable to construct case statement")
}
query, args, err := s.getQueryBuilder().Select(`Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.OriginalId AS PostOriginalId,
Posts.RootId AS PostRootId,
Posts.FileIds AS PostFileIds,
Teams.Id AS TeamId,
Teams.Name AS TeamName,
builder := s.getQueryBuilder().Select(`Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.EditAt AS PostEditAt,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.OriginalId AS PostOriginalId,
Posts.RootId AS PostRootId,
Posts.FileIds AS PostFileIds,
Teams.Id AS TeamId,
Teams.Name AS TeamName,
Teams.DisplayName AS TeamDisplayName,
Channels.Id AS ChannelId,
Channels.Name AS ChannelName,
Channels.Type AS ChannelType,
Users.Id AS UserId,
Users.Email AS UserEmail,
Channels.Id AS ChannelId,
Channels.Name AS ChannelName,
Channels.Type AS ChannelType,
Users.Id AS UserId,
Users.Email AS UserEmail,
Users.Username,
Bots.UserId IS NOT NULL AS IsBot`).
Column(caseStmt+" AS ChannelDisplayName", caseArgs...).
@@ -323,8 +324,13 @@ func (s SqlComplianceStore) MessageExport(c request.CTX, cursor model.MessageExp
sq.NotLike{"Posts.Type": "system_%"},
}).
OrderBy("PostUpdateAt, PostId").
Limit(uint64(limit)).
ToSql()
Limit(uint64(limit))
if cursor.UntilUpdateAt > 0 {
builder = builder.Where(sq.LtOrEq{"Posts.UpdateAt": cursor.UntilUpdateAt})
}
query, args, err := builder.ToSql()
if err != nil {
return nil, cursor, errors.Wrap(err, "unable to construct query to export messages")
}

Просмотреть файл

@@ -2404,15 +2404,14 @@ func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int6
})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "post_tosql")
if options.UntilUpdateAt > 0 {
query = query.Where(sq.LtOrEq{"p.UpdateAt": options.UntilUpdateAt})
}
var v int64
err = s.GetReplica().Get(&v, queryString, args...)
err := s.GetReplica().GetBuilder(&v, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count Posts")
return 0, fmt.Errorf("post_tosql failed or failed to count Posts: %w", err)
}
return v, nil

Просмотреть файл

@@ -35,6 +35,8 @@ import (
type migrationDirection string
type Option func(s *SqlStore) error
const (
IndexTypeFullText = "full_text"
IndexTypeFullTextFunc = "full_text_func"
@@ -139,12 +141,20 @@ type SqlStore struct {
isBinaryParam bool
pgDefaultTextSearchConfig string
skipMigrations bool
quitMonitor chan struct{}
wgMonitor *sync.WaitGroup
}
func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterfaces.MetricsInterface) (*SqlStore, error) {
func SkipMigrations() Option {
return func(s *SqlStore) error {
s.skipMigrations = true
return nil
}
}
func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterfaces.MetricsInterface, options ...Option) (*SqlStore, error) {
store := &SqlStore{
rrCounter: 0,
srCounter: 0,
@@ -155,6 +165,12 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
wgMonitor: &sync.WaitGroup{},
}
for _, option := range options {
if err := option(store); err != nil {
return nil, fmt.Errorf("failed to apply option: %w", err)
}
}
err := store.initConnection()
if err != nil {
return nil, errors.Wrap(err, "error setting up connections")
@@ -178,9 +194,11 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
return nil, errors.Wrap(err, "error while checking DB collation")
}
err = store.migrate(migrationsDirectionUp, false)
if err != nil {
return nil, errors.Wrap(err, "failed to apply database migrations")
if !store.skipMigrations {
err = store.migrate(migrationsDirectionUp, false)
if err != nil {
return nil, errors.Wrap(err, "failed to apply database migrations")
}
}
store.isBinaryParam, err = store.computeBinaryParam()

Просмотреть файл

@@ -4,6 +4,7 @@
package sqlstore
import (
"database/sql"
"fmt"
"os"
"path/filepath"
@@ -977,3 +978,40 @@ func TestGetAppliedMigrations(t *testing.T) {
})
}
}
func TestSkipMigrationsOption(t *testing.T) {
testDrivers := []string{
model.DatabaseDriverPostgres,
model.DatabaseDriverMysql,
}
logger := mlog.CreateConsoleTestLogger(t)
for _, d := range testDrivers {
driver := d
t.Run("Should not apply migrations when skipMigrations=true for "+driver, func(t *testing.T) {
t.Parallel()
settings, err := makeSqlSettings(driver)
if err != nil {
t.Skip(err)
}
store, err := New(*settings, logger, nil, SkipMigrations())
require.NoError(t, err)
_, err = store.GetDBSchemaVersion()
assert.Error(t, err)
// Unfortunately there's no error for table doesn't exist, but we know it won't be ErrNoRows:
assert.True(t, !errors.Is(err, sql.ErrNoRows))
// And we know what each db will return:
if driver == model.DatabaseDriverPostgres {
assert.Contains(t, err.Error(), "pq: relation \"db_migrations\" does not exist")
} else {
// Mysql includes the random db name, so test the end:
assert.Contains(t, err.Error(), ".db_migrations' doesn't exist")
}
})
}
}

Просмотреть файл

@@ -318,7 +318,8 @@ type ChannelStore interface {
type ChannelMemberHistoryStore interface {
LogJoinEvent(userID string, channelID string, joinTime int64) error
LogLeaveEvent(userID string, channelID string, leaveTime int64) error
GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error)
GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error)
GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)

Просмотреть файл

@@ -4,9 +4,9 @@
package storetest
import (
"testing"
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -21,6 +21,7 @@ func TestChannelMemberHistoryStore(t *testing.T, rctx request.CTX, ss store.Stor
t.Run("TestLogLeaveEvent", func(t *testing.T) { testLogLeaveEvent(t, rctx, ss) })
t.Run("TestGetUsersInChannelAtChannelMemberHistory", func(t *testing.T) { testGetUsersInChannelAtChannelMemberHistory(t, rctx, ss) })
t.Run("TestGetUsersInChannelAtChannelMembers", func(t *testing.T) { testGetUsersInChannelAtChannelMembers(t, rctx, ss) })
t.Run("TestGetChannelsWithActivityDuring", func(t *testing.T) { testGetChannelsWithActivityDuring(t, rctx, ss) })
t.Run("TestPermanentDeleteBatch", func(t *testing.T) { testPermanentDeleteBatch(t, rctx, ss) })
t.Run("TestPermanentDeleteBatchForRetentionPolicies", func(t *testing.T) { testPermanentDeleteBatchForRetentionPolicies(t, rctx, ss) })
t.Run("TestGetChannelsLeftSince", func(t *testing.T) { testGetChannelsLeftSince(t, rctx, ss) })
@@ -81,6 +82,160 @@ func testLogLeaveEvent(t *testing.T, rctx request.CTX, ss store.Store) {
assert.NoError(t, err)
}
func testGetChannelsWithActivityDuring(t *testing.T, rctx request.CTX, ss store.Store) {
// Need to wait to make sure channels and posts have nothing in them for this test.
time.Sleep(101 * time.Millisecond)
// create three test channels
ch1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel1, err := ss.Channel().Save(rctx, ch1, -1)
require.NoError(t, err)
// channel2 will have no activity until case 6 (shouldn't show up until then)
ch2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel2, err := ss.Channel().Save(rctx, ch2, -1)
require.NoError(t, err)
// and two test users
user1 := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewUsername(),
}
userPtr, err := ss.User().Save(rctx, &user1)
require.NoError(t, err)
user1 = *userPtr
user2 := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewUsername(),
}
userPtr, err = ss.User().Save(rctx, &user2)
require.NoError(t, err)
user2 = *userPtr
now := model.GetMillis()
originalNow := now
// user2 joins channel2 before test
err = ss.ChannelMemberHistory().LogJoinEvent(user2.Id, channel2.Id, now-2000)
require.NoError(t, err)
// case 7: assert no activity for time period before channel activity
channelIds, err := ss.ChannelMemberHistory().GetChannelsWithActivityDuring(originalNow-100, originalNow)
require.NoError(t, err)
assert.Empty(t, channelIds)
// case 1: user1 was in channel before period, doesn't show activity
err = ss.ChannelMemberHistory().LogJoinEvent(user1.Id, channel1.Id, now-1100)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now, now+1000)
require.NoError(t, err)
assert.Empty(t, channelIds)
// case 2: user1 leaves, shows activity
err = ss.ChannelMemberHistory().LogLeaveEvent(user1.Id, channel1.Id, now+1)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now, now+1000)
require.NoError(t, err)
assert.Equal(t, channelIds, []string{channel1.Id})
// case 3: user1 joins, shows activity
err = ss.ChannelMemberHistory().LogJoinEvent(user1.Id, channel1.Id, now+2)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now+2, now+1000)
require.NoError(t, err)
assert.Equal(t, channelIds, []string{channel1.Id})
// case 4: new post shows activity
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now+3, now+1000)
require.NoError(t, err)
assert.Empty(t, channelIds)
post := &model.Post{
ChannelId: channel1.Id,
Message: "root post",
UserId: user1.Id,
CreateAt: now + 4,
UpdateAt: now + 4,
}
post, err = ss.Post().Save(rctx, post)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now+3, now+1000)
require.NoError(t, err)
assert.Equal(t, channelIds, []string{channel1.Id})
// case 5: update shows activity
// need to wait because update uses getMillis
time.Sleep(10 * time.Millisecond)
now = model.GetMillis()
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now-1, now+1000)
require.NoError(t, err)
assert.Empty(t, channelIds)
newPost := post.Clone()
newPost.Message = "edited message"
_, err = ss.Post().Update(rctx, newPost, post)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now-1, now+1000)
require.NoError(t, err)
assert.Equal(t, channelIds, []string{channel1.Id})
// case 6: get both activity from posts and from join/leave;
// - also, sql deduplicates two channel1 results (from post and channel history tables)
time.Sleep(1 * time.Millisecond)
now = model.GetMillis()
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now, now+1000)
require.NoError(t, err)
assert.Empty(t, channelIds)
post2 := &model.Post{
ChannelId: channel1.Id,
Message: "root post",
UserId: user1.Id,
CreateAt: now + 11,
UpdateAt: now + 11,
}
_, err = ss.Post().Save(rctx, post2)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user1.Id, channel1.Id, now+12)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user2.Id, channel2.Id, now+13)
require.NoError(t, err)
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now+10, now+1000)
require.NoError(t, err)
assert.ElementsMatch(t, channelIds, []string{channel1.Id, channel2.Id})
// case 7: still no activity for period before tests
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(originalNow-100, originalNow)
require.NoError(t, err)
assert.Empty(t, channelIds)
// case 8: no activity for period after tests
channelIds, err = ss.ChannelMemberHistory().GetChannelsWithActivityDuring(now+100, now+1000)
require.NoError(t, err)
assert.Empty(t, channelIds)
}
func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX, ss store.Store) {
// create a test channel
ch := &model.Channel{
@@ -119,12 +274,12 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX,
require.NoError(t, err)
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, []string{channel.Id})
require.NoError(t, err)
assert.Empty(t, channelMembers)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -135,7 +290,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX,
assert.Nil(t, channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -150,7 +305,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX,
require.NoError(t, err)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -161,7 +316,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX,
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -172,7 +327,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, rctx request.CTX,
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, []string{channel.Id})
require.NoError(t, err)
assert.Empty(t, channelMembers)
}
@@ -223,7 +378,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
// the past, even though the time that they were actually in the channel doesn't necessarily overlap with the export period
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -234,7 +389,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -245,7 +400,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -256,7 +411,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -267,7 +422,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -278,7 +433,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st
assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
@@ -332,7 +487,7 @@ func testPermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store.Store) {
require.NoError(t, err)
// in between the join time and the leave time, both users were members of the channel
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 2)
@@ -343,7 +498,7 @@ func testPermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store.Store) {
assert.NotEqual(t, int64(0), rowsDeleted)
// after the delete, there should be one less member in the channel
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, []string{channel.Id})
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, user2.Id, channelMembers[0].UserId)
@@ -387,7 +542,7 @@ func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx request.CTX
_, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, []string{channel.Id})
require.NoError(t, err)
require.Empty(t, result, "history should have been deleted by channel policy")
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("ChannelMemberHistory", 1000)

Просмотреть файл

@@ -58,6 +58,7 @@ func TestComplianceStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("MessageEditAfterExportMessage", func(t *testing.T) { testEditAfterExportMessage(t, rctx, ss) })
t.Run("MessageDeleteExportMessage", func(t *testing.T) { testDeleteExportMessage(t, rctx, ss) })
t.Run("MessageDeleteAfterExportMessage", func(t *testing.T) { testDeleteAfterExportMessage(t, rctx, ss) })
t.Run("MessageExport_UntilUpdateAt", func(t *testing.T) { testMessageExportUntilUpdateAt(t, rctx, ss) })
}
func testComplianceStore(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -1174,3 +1175,105 @@ func testDeleteAfterExportMessage(t *testing.T, rctx request.CTX, ss store.Store
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
func testMessageExportUntilUpdateAt(t *testing.T, rctx request.CTX, ss store.Store) {
defer cleanupStoreState(t, rctx, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(rctx, model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
user1, err = ss.User().Save(rctx, user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
}
user2, err = ss.User().Save(rctx, user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(rctx, channel, -1)
require.NoError(t, nErr)
var posts []*model.Post
// user1 posts ten times in the public channel
for i := 0; i < 10; i++ {
post := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime + int64(i),
UpdateAt: startTime + int64(i),
Message: model.NewId(),
}
post, err = ss.Post().Save(rctx, post)
require.NoError(t, err)
posts = append(posts, post)
}
// fetch 5 starting from the third post, using LastPostUpdateAt and UntilUpdateAt.
// UntilUpdateAt is inclusive
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(rctx, model.MessageExportCursor{LastPostUpdateAt: posts[2].UpdateAt, UntilUpdateAt: posts[2].UpdateAt + 4}, 10000)
require.NoError(t, err)
assert.Equal(t, 5, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
for i := 2; i < 7; i++ {
assert.Equal(t, posts[i].Id, *messageExportMap[posts[i].Id].PostId)
assert.Equal(t, posts[i].CreateAt, *messageExportMap[posts[i].Id].PostCreateAt)
assert.Equal(t, posts[i].Message, *messageExportMap[posts[i].Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[posts[i].Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[posts[i].Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[posts[i].Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[posts[i].Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[posts[i].Id].Username)
}
// Also test AnalyticsPostCount because they are used in tandem for MessageExports
count, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{
TeamId: channel.TeamId,
ExcludeSystemPosts: true,
SinceUpdateAt: posts[2].UpdateAt,
UntilUpdateAt: posts[2].UpdateAt + 4,
})
require.NoError(t, err)
require.Equal(t, 5, int(count))
}

Просмотреть файл

@@ -72,8 +72,38 @@ func (_m *ChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since i
return r0, r1
}
// GetChannelsWithActivityDuring provides a mock function with given fields: startTime, endTime
func (_m *ChannelMemberHistoryStore) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) {
ret := _m.Called(startTime, endTime)
if len(ret) == 0 {
panic("no return value specified for GetChannelsWithActivityDuring")
}
var r0 []string
var r1 error
if rf, ok := ret.Get(0).(func(int64, int64) ([]string, error)); ok {
return rf(startTime, endTime)
}
if rf, ok := ret.Get(0).(func(int64, int64) []string); ok {
r0 = rf(startTime, endTime)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(startTime, endTime)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersInChannelDuring provides a mock function with given fields: startTime, endTime, channelID
func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error) {
ret := _m.Called(startTime, endTime, channelID)
if len(ret) == 0 {
@@ -82,10 +112,10 @@ func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, en
var r0 []*model.ChannelMemberHistoryResult
var r1 error
if rf, ok := ret.Get(0).(func(int64, int64, string) ([]*model.ChannelMemberHistoryResult, error)); ok {
if rf, ok := ret.Get(0).(func(int64, int64, []string) ([]*model.ChannelMemberHistoryResult, error)); ok {
return rf(startTime, endTime, channelID)
}
if rf, ok := ret.Get(0).(func(int64, int64, string) []*model.ChannelMemberHistoryResult); ok {
if rf, ok := ret.Get(0).(func(int64, int64, []string) []*model.ChannelMemberHistoryResult); ok {
r0 = rf(startTime, endTime, channelID)
} else {
if ret.Get(0) != nil {
@@ -93,7 +123,7 @@ func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, en
}
}
if rf, ok := ret.Get(1).(func(int64, int64, string) error); ok {
if rf, ok := ret.Get(1).(func(int64, int64, []string) error); ok {
r1 = rf(startTime, endTime, channelID)
} else {
r1 = ret.Error(1)

Просмотреть файл

@@ -2696,7 +2696,23 @@ func (s *TimerLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
func (s *TimerLayerChannelMemberHistoryStore) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetChannelsWithActivityDuring(startTime, endTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetChannelsWithActivityDuring", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)