Mm 30807 granular data retention scaffold (#17464)

Этот коммит содержится в:
Max Erenberg
2021-04-20 13:16:40 -04:00
коммит произвёл GitHub
родитель 368b642105
Коммит f36f5c74b1
49 изменённых файлов: 5079 добавлений и 984 удалений

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

@@ -1036,6 +1036,9 @@ func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCo
selectStr = "count(c.Id)"
} else {
selectStr = "c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt"
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId"
}
}
query := s.getQueryBuilder().
@@ -1059,6 +1062,13 @@ func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCo
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if opts.ExcludePolicyConstrained || opts.IncludePolicyID {
query = query.LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
if opts.ExcludePolicyConstrained {
query = query.Where("RetentionPoliciesChannels.ChannelId IS NULL")
}
return query
}
@@ -2660,7 +2670,7 @@ func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term
})
}
func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearchOpts, countQuery bool) sq.SelectBuilder {
func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.SelectBuilder {
var limit int
if opts.PerPage != nil {
limit = *opts.PerPage
@@ -2669,10 +2679,16 @@ func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearc
}
var selectStr string
if countQuery {
if opts.CountOnly {
selectStr = "count(*)"
} else {
selectStr = "c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"
selectStr = "c.*"
if opts.IncludeTeamInfo {
selectStr += ", t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"
}
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId"
}
}
query := s.getQueryBuilder().
@@ -2681,7 +2697,7 @@ func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearc
Join("Teams AS t ON t.Id = c.TeamId")
// don't bother ordering or limiting if we're just getting the count
if !countQuery {
if !opts.CountOnly {
query = query.
OrderBy("c.DisplayName, t.DisplayName").
Limit(uint64(limit))
@@ -2692,14 +2708,27 @@ func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearc
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.IsPaginated() && !countQuery {
if opts.IsPaginated() && !opts.CountOnly {
query = query.Offset(uint64(*opts.Page * *opts.PerPage))
}
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
if opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": opts.PolicyID})
} else if opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where("RetentionPoliciesChannels.ChannelId IS NULL")
} else if opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
likeClause, likeTerm := s.buildLIKEClause(opts.Term, "c.Name, c.DisplayName, c.Purpose")
if likeTerm != "" {
likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "?")
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose")
fulltextClause, fulltextTerm := s.buildFulltextClause(opts.Term, "c.Name, c.DisplayName, c.Purpose")
fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "?")
query = query.Where(sq.Or{
sq.Expr(likeClause, likeTerm, likeTerm, likeTerm), // Keep the number of likeTerms same as the number
@@ -2730,7 +2759,7 @@ func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearc
}
if opts.Public && !opts.Private {
query = query.Where(sq.Eq{"c.Type": model.CHANNEL_OPEN})
query = query.InnerJoin("PublicChannels ON c.Id = PublicChannels.Id")
} else if opts.Private && !opts.Public {
query = query.Where(sq.Eq{"c.Type": model.CHANNEL_PRIVATE})
} else {
@@ -2744,7 +2773,9 @@ func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearc
}
func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (*model.ChannelListWithTeamData, int64, error) {
queryString, args, err := s.channelSearchQuery(term, opts, false).ToSql()
opts.Term = term
opts.IncludeTeamInfo = true
queryString, args, err := s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}
@@ -2757,7 +2788,8 @@ func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearch
// only query a 2nd time for the count if the results are being requested paginated.
if opts.IsPaginated() {
queryString, args, err = s.channelSearchQuery(term, opts, true).ToSql()
opts.CountOnly = true
queryString, args, err = s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}

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

@@ -31,8 +31,8 @@ func TestChannelSearchQuerySQLInjection(t *testing.T) {
SqlStore: st.SqlStore,
}
opts := store.ChannelSearchOpts{}
builder := s.channelSearchQuery("'or'1'=sleep(3))); -- -", opts, false)
opts := store.ChannelSearchOpts{Term: "'or'1'=sleep(3))); -- -"}
builder := s.channelSearchQuery(&opts)
query, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, query, "sleep")

609
store/sqlstore/retention_policy_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,609 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/mattermost/gorp"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/pkg/errors"
)
type SqlRetentionPolicyStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlRetentionPolicyStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.RetentionPolicyStore {
s := &SqlRetentionPolicyStore{
SqlStore: sqlStore,
metrics: metrics,
}
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.RetentionPolicy{}, "RetentionPolicies")
table.SetKeys(false, "Id")
table.ColMap("Id").SetMaxSize(26)
table.ColMap("DisplayName").SetMaxSize(64)
tableC := db.AddTableWithName(model.RetentionPolicyChannel{}, "RetentionPoliciesChannels")
tableC.SetKeys(false, "ChannelId")
tableC.ColMap("PolicyId").SetMaxSize(26)
tableC.ColMap("ChannelId").SetMaxSize(26)
tableT := db.AddTableWithName(model.RetentionPolicyTeam{}, "RetentionPoliciesTeams")
tableT.SetKeys(false, "TeamId")
tableT.ColMap("PolicyId").SetMaxSize(26)
tableT.ColMap("TeamId").SetMaxSize(26)
}
return s
}
func (s *SqlRetentionPolicyStore) createIndexesIfNotExists() {
s.CreateCompositeIndexIfNotExists("IDX_RetentionPolicies_DisplayName_Id", "RetentionPolicies",
[]string{"DisplayName", "Id"})
s.CreateIndexIfNotExists("IDX_RetentionPoliciesChannels_PolicyId", "RetentionPoliciesChannels", "PolicyId")
s.CreateIndexIfNotExists("IDX_RetentionPoliciesTeams_PolicyId", "RetentionPoliciesTeams", "PolicyId")
s.CreateForeignKeyIfNotExists("RetentionPoliciesChannels", "PolicyId", "RetentionPolicies", "Id", true)
s.CreateForeignKeyIfNotExists("RetentionPoliciesTeams", "PolicyId", "RetentionPolicies", "Id", true)
}
// executePossiblyEmptyQuery only executes the query if it is non-empty. This helps avoid
// having to check for MySQL, which, unlike Postgres, does not allow empty queries.
func executePossiblyEmptyQuery(txn *gorp.Transaction, query string, args ...interface{}) (sql.Result, error) {
if query == "" {
return nil, nil
}
return txn.Exec(query, args...)
}
func (s *SqlRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
// Strategy:
// 1. Insert new policy
// 2. Insert new channels into policy
// 3. Insert new teams into policy
if err := s.checkTeamsExist(policy.TeamIDs); err != nil {
return nil, err
}
if err := s.checkChannelsExist(policy.ChannelIDs); err != nil {
return nil, err
}
policy.ID = model.NewId()
policyInsertQuery, policyInsertArgs, err := s.getQueryBuilder().
Insert("RetentionPolicies").
Columns("Id", "DisplayName", "PostDuration").
Values(policy.ID, policy.DisplayName, policy.PostDuration).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err := s.buildInsertRetentionPoliciesChannelsQuery(policy.ID, policy.ChannelIDs)
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err := s.buildInsertRetentionPoliciesTeamsQuery(policy.ID, policy.TeamIDs)
if err != nil {
return nil, err
}
policySelectQuery, policySelectProps := s.buildGetPolicyQuery(policy.ID)
txn, err := s.GetMaster().Begin()
if err != nil {
return nil, err
}
defer finalizeTransaction(txn)
// Create a new policy in RetentionPolicies
if _, err = txn.Exec(policyInsertQuery, policyInsertArgs...); err != nil {
return nil, err
}
// Insert the channel IDs into RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Insert the team IDs into RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the new policy (with team/channel counts) which we just created
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.SelectOne(&newPolicy, policySelectQuery, policySelectProps); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) checkTeamsExist(teamIDs []string) error {
if len(teamIDs) > 0 {
teamsSelectQuery, teamsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Teams").
Where(sq.Eq{"Id": teamIDs}).
ToSql()
if err != nil {
return err
}
var rows []*string
_, err = s.GetReplica().Select(&rows, teamsSelectQuery, teamsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(teamIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, teamID := range rows {
retrievedIDs[*teamID] = true
}
for _, teamID := range teamIDs {
if _, ok := retrievedIDs[teamID]; !ok {
return store.NewErrNotFound("Team", teamID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) checkChannelsExist(channelIDs []string) error {
if len(channelIDs) > 0 {
channelsSelectQuery, channelsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Channels").
Where(sq.Eq{"Id": channelIDs}).
ToSql()
if err != nil {
return err
}
var rows []*string
_, err = s.GetReplica().Select(&rows, channelsSelectQuery, channelsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(channelIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, channelID := range rows {
retrievedIDs[*channelID] = true
}
for _, channelID := range channelIDs {
if _, ok := retrievedIDs[channelID]; !ok {
return store.NewErrNotFound("Channel", channelID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesChannelsQuery(policyID string, channelIDs []string) (query string, args []interface{}, err error) {
if len(channelIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("PolicyId", "ChannelId")
for _, channelID := range channelIDs {
builder = builder.Values(policyID, channelID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesTeamsQuery(policyID string, teamIDs []string) (query string, args []interface{}, err error) {
if len(teamIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamID := range teamIDs {
builder = builder.Values(policyID, teamID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
// Strategy:
// 1. Update policy attributes
// 2. Delete existing channels from policy
// 3. Insert new channels into policy
// 4. Delete existing teams from policy
// 5. Insert new teams into policy
// 6. Read new policy
var err error
if err = s.checkTeamsExist(patch.TeamIDs); err != nil {
return nil, err
}
if err = s.checkChannelsExist(patch.ChannelIDs); err != nil {
return nil, err
}
policyUpdateQuery := ""
policyUpdateArgs := []interface{}{}
if patch.DisplayName != "" || patch.PostDuration != nil {
builder := s.getQueryBuilder().Update("RetentionPolicies")
if patch.DisplayName != "" {
builder = builder.Set("DisplayName", patch.DisplayName)
}
if patch.PostDuration != nil {
builder = builder.Set("PostDuration", *patch.PostDuration)
}
policyUpdateQuery, policyUpdateArgs, err = builder.
Where(sq.Eq{"Id": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
}
channelsDeleteQuery := ""
channelsDeleteArgs := []interface{}{}
channelsInsertQuery := ""
channelsInsertArgs := []interface{}{}
if patch.ChannelIDs != nil {
channelsDeleteQuery, channelsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err = s.buildInsertRetentionPoliciesChannelsQuery(patch.ID, patch.ChannelIDs)
if err != nil {
return nil, err
}
}
teamsDeleteQuery := ""
teamsDeleteArgs := []interface{}{}
teamsInsertQuery := ""
teamsInsertArgs := []interface{}{}
if patch.TeamIDs != nil {
teamsDeleteQuery, teamsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err = s.buildInsertRetentionPoliciesTeamsQuery(patch.ID, patch.TeamIDs)
if err != nil {
return nil, err
}
}
policySelectQuery, policySelectProps := s.buildGetPolicyQuery(patch.ID)
txn, err := s.GetMaster().Begin()
if err != nil {
return nil, err
}
defer finalizeTransaction(txn)
// Update the fields of the policy in RetentionPolicies
if _, err = executePossiblyEmptyQuery(txn, policyUpdateQuery, policyUpdateArgs...); err != nil {
return nil, err
}
// Remove all channels from the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsDeleteQuery, channelsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new channels for the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Remove all teams from the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsDeleteQuery, teamsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new teams for the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the policy which we just updated
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.SelectOne(&newPolicy, policySelectQuery, policySelectProps); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) buildGetPolicyQuery(id string) (query string, props map[string]interface{}) {
return s.buildGetPoliciesQuery(id, 0, 1)
}
// buildGetPoliciesQuery builds a query to select information for the policy with the specified
// ID, or, if `id` is the empty string, from all policies. The results returned will be sorted by
// policy display name and ID.
func (s *SqlRetentionPolicyStore) buildGetPoliciesQuery(id string, offset, limit int) (query string, props map[string]interface{}) {
props = map[string]interface{}{"Offset": offset, "Limit": limit}
whereIdEqualsPolicyId := ""
if id != "" {
whereIdEqualsPolicyId = "WHERE RetentionPolicies.Id = :PolicyId"
props["PolicyId"] = id
}
query = `
SELECT RetentionPolicies.Id,
RetentionPolicies.DisplayName,
RetentionPolicies.PostDuration,
A.Count AS ChannelCount,
B.Count AS TeamCount
FROM RetentionPolicies
INNER JOIN (
SELECT RetentionPolicies.Id,
COUNT(RetentionPoliciesChannels.ChannelId) AS Count
FROM RetentionPolicies
LEFT JOIN RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId
` + whereIdEqualsPolicyId + `
GROUP BY RetentionPolicies.Id
ORDER BY RetentionPolicies.DisplayName, RetentionPolicies.Id
LIMIT :Limit
OFFSET :Offset
) AS A ON RetentionPolicies.Id = A.Id
INNER JOIN (
SELECT RetentionPolicies.Id,
COUNT(RetentionPoliciesTeams.TeamId) AS Count
FROM RetentionPolicies
LEFT JOIN RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId
` + whereIdEqualsPolicyId + `
GROUP BY RetentionPolicies.Id
ORDER BY RetentionPolicies.DisplayName, RetentionPolicies.Id
LIMIT :Limit
OFFSET :Offset
) AS B ON RetentionPolicies.Id = B.Id
ORDER BY RetentionPolicies.DisplayName, RetentionPolicies.Id`
return
}
func (s *SqlRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
query, props := s.buildGetPolicyQuery(id)
var policy model.RetentionPolicyWithTeamAndChannelCounts
if err := s.GetReplica().SelectOne(&policy, query, props); err != nil {
return nil, err
}
return &policy, nil
}
func (s *SqlRetentionPolicyStore) GetAll(offset, limit int) (policies []*model.RetentionPolicyWithTeamAndChannelCounts, err error) {
query, props := s.buildGetPoliciesQuery("", offset, limit)
_, err = s.GetReplica().Select(&policies, query, props)
return
}
func (s *SqlRetentionPolicyStore) GetCount() (int64, error) {
return s.GetReplica().SelectInt("SELECT COUNT(*) FROM RetentionPolicies")
}
func (s *SqlRetentionPolicyStore) Delete(id string) error {
builder := s.getQueryBuilder().
Delete("RetentionPolicies").
Where(sq.Eq{"Id": id})
result, err := builder.RunWith(s.GetMaster()).Exec()
if err != nil {
return err
}
numRowsAffected, err := result.RowsAffected()
if err != nil {
return err
} else if numRowsAffected == 0 {
return errors.New("policy not found")
}
return nil
}
func (s *SqlRetentionPolicyStore) GetChannels(policyId string, offset, limit int) (channels model.ChannelListWithTeamData, err error) {
const query = `
SELECT Channels.*,
Teams.DisplayName AS TeamDisplayName,
Teams.Name AS TeamName,
Teams.UpdateAt AS TeamUpdateAt
FROM RetentionPoliciesChannels
INNER JOIN Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id
INNER JOIN Teams ON Channels.TeamId = Teams.Id
WHERE RetentionPoliciesChannels.PolicyId = :PolicyId
ORDER BY Channels.DisplayName, Channels.Id
LIMIT :Limit
OFFSET :Offset`
props := map[string]interface{}{"PolicyId": policyId, "Limit": limit, "Offset": offset}
_, err = s.GetReplica().Select(&channels, query, props)
for _, channel := range channels {
channel.PolicyID = model.NewString(policyId)
}
return
}
func (s *SqlRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
const query = `
SELECT COUNT(*)
FROM RetentionPolicies
INNER JOIN RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId
WHERE RetentionPolicies.Id = :PolicyId`
props := map[string]interface{}{"PolicyId": policyId}
return s.GetReplica().SelectInt(query, props)
}
func (s *SqlRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
if err := s.checkChannelsExist(channelIds); err != nil {
return err
}
builder := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("policyId", "channelId")
for _, channelId := range channelIds {
builder = builder.Values(policyId, channelId)
}
_, err := builder.RunWith(s.GetMaster()).Exec()
if err != nil {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
case *mysql.MySQLError:
if dbErr.Number == MySQLForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
}
}
return err
}
func (s *SqlRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
builder := s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"ChannelId": channelIds},
})
_, err := builder.RunWith(s.GetMaster()).Exec()
return err
}
func (s *SqlRetentionPolicyStore) GetTeams(policyId string, offset, limit int) (teams []*model.Team, err error) {
const query = `
SELECT Teams.* FROM RetentionPoliciesTeams
INNER JOIN Teams ON RetentionPoliciesTeams.TeamId = Teams.Id
WHERE RetentionPoliciesTeams.PolicyId = :PolicyId
ORDER BY Teams.DisplayName, Teams.Id
LIMIT :Limit
OFFSET :Offset`
props := map[string]interface{}{"PolicyId": policyId, "Limit": limit, "Offset": offset}
_, err = s.GetReplica().Select(&teams, query, props)
for _, team := range teams {
team.PolicyID = &policyId
}
return
}
func (s *SqlRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
const query = `
SELECT COUNT(*)
FROM RetentionPolicies
INNER JOIN RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId
WHERE RetentionPolicies.Id = :PolicyId`
props := map[string]interface{}{"PolicyId": policyId}
return s.GetReplica().SelectInt(query, props)
}
func (s *SqlRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
if err := s.checkTeamsExist(teamIds); err != nil {
return err
}
builder := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamId := range teamIds {
builder = builder.Values(policyId, teamId)
}
_, err := builder.RunWith(s.GetMaster()).Exec()
return err
}
func (s *SqlRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
builder := s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"TeamId": teamIds},
})
_, err := builder.RunWith(s.GetMaster()).Exec()
return err
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset, limit int) (policies []*model.RetentionPolicyForTeam, err error) {
const query = `
SELECT Teams.Id, RetentionPolicies.PostDuration
FROM Users
INNER JOIN TeamMembers ON Users.Id = TeamMembers.UserId
INNER JOIN Teams ON TeamMembers.TeamId = Teams.Id
INNER JOIN RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId
INNER JOIN RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id
WHERE Users.Id = :UserId
AND TeamMembers.DeleteAt = 0
AND Teams.DeleteAt = 0
ORDER BY Teams.Id
LIMIT :Limit
OFFSET :Offset`
props := map[string]interface{}{"UserId": userID, "Limit": limit, "Offset": offset}
_, err = s.GetReplica().Select(&policies, query, props)
return
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
const query = `
SELECT COUNT(*)
FROM Users
INNER JOIN TeamMembers ON Users.Id = TeamMembers.UserId
INNER JOIN Teams ON TeamMembers.TeamId = Teams.Id
INNER JOIN RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId
INNER JOIN RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id
WHERE Users.Id = :UserId
AND TeamMembers.DeleteAt = 0
AND Teams.DeleteAt = 0`
props := map[string]interface{}{"UserId": userID}
return s.GetReplica().SelectInt(query, props)
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset, limit int) (policies []*model.RetentionPolicyForChannel, err error) {
const query = `
SELECT Channels.Id, RetentionPolicies.PostDuration
FROM Users
INNER JOIN ChannelMembers ON Users.Id = ChannelMembers.UserId
INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id
INNER JOIN RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId
INNER JOIN RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id
WHERE Users.Id = :UserId
AND Channels.DeleteAt = 0
ORDER BY Channels.Id
LIMIT :Limit
OFFSET :Offset`
props := map[string]interface{}{"UserId": userID, "Limit": limit, "Offset": offset}
_, err = s.GetReplica().Select(&policies, query, props)
return
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
const query = `
SELECT COUNT(*)
FROM Users
INNER JOIN ChannelMembers ON Users.Id = ChannelMembers.UserId
INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id
INNER JOIN RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId
INNER JOIN RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id
WHERE Users.Id = :UserId
AND Channels.DeleteAt = 0`
props := map[string]interface{}{"UserId": userID}
return s.GetReplica().SelectInt(query, props)
}

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

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost-server/v5/store/storetest"
)
func TestRetentionPolicyStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestRetentionPolicyStore)
}

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

@@ -41,13 +41,17 @@ import (
type migrationDirection string
const (
IndexTypeFullText = "full_text"
IndexTypeFullTextFunc = "full_text_func"
IndexTypeDefault = "default"
PGDupTableErrorCode = "42P07" // see https://github.com/lib/pq/blob/master/error.go#L268
MySQLDupTableErrorCode = uint16(1050) // see https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html#error_er_table_exists_error
DBPingAttempts = 18
DBPingTimeoutSecs = 10
IndexTypeFullText = "full_text"
IndexTypeFullTextFunc = "full_text_func"
IndexTypeDefault = "default"
PGDupTableErrorCode = "42P07" // see https://github.com/lib/pq/blob/master/error.go#L268
MySQLDupTableErrorCode = uint16(1050) // see https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html#error_er_table_exists_error
PGForeignKeyViolationErrorCode = "23503"
MySQLForeignKeyViolationErrorCode = 1452
PGDuplicateObjectErrorCode = "42710"
MySQLDuplicateObjectErrorCode = 1022
DBPingAttempts = 18
DBPingTimeoutSecs = 10
// This is a numerical version string by postgres. The format is
// 2 characters for major, minor, and patch version prior to 10.
// After 10, it's major and minor only.
@@ -95,6 +99,7 @@ type SqlStoreStores struct {
team store.TeamStore
channel store.ChannelStore
post store.PostStore
retentionPolicy store.RetentionPolicyStore
thread store.ThreadStore
user store.UserStore
bot store.BotStore
@@ -183,6 +188,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.team = newSqlTeamStore(store)
store.stores.channel = newSqlChannelStore(store, metrics)
store.stores.post = newSqlPostStore(store, metrics)
store.stores.retentionPolicy = newSqlRetentionPolicyStore(store, metrics)
store.stores.user = newSqlUserStore(store, metrics)
store.stores.bot = newSqlBotStore(store, metrics)
store.stores.audit = newSqlAuditStore(store)
@@ -236,6 +242,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.channel.(*SqlChannelStore).createIndexesIfNotExists()
store.stores.post.(*SqlPostStore).createIndexesIfNotExists()
store.stores.retentionPolicy.(*SqlRetentionPolicyStore).createIndexesIfNotExists()
store.stores.thread.(*SqlThreadStore).createIndexesIfNotExists()
store.stores.user.(*SqlUserStore).createIndexesIfNotExists()
store.stores.bot.(*SqlBotStore).createIndexesIfNotExists()
@@ -1076,6 +1083,30 @@ func (ss *SqlStore) createIndexIfNotExists(indexName string, tableName string, c
return true
}
func (ss *SqlStore) CreateForeignKeyIfNotExists(
tableName, columnName, refTableName, refColumnName string,
onDeleteCascade bool,
) (err error) {
deleteClause := ""
if onDeleteCascade {
deleteClause = "ON DELETE CASCADE"
}
constraintName := "FK_" + tableName + "_" + refTableName
sQuery := `
ALTER TABLE ` + tableName + `
ADD CONSTRAINT ` + constraintName + `
FOREIGN KEY (` + columnName + `) REFERENCES ` + refTableName + ` (` + refColumnName + `)
` + deleteClause + `;`
_, err = ss.GetMaster().ExecNoTimeout(sQuery)
if IsConstraintAlreadyExistsError(err) {
err = nil
}
if err != nil {
mlog.Warn("Could not create foreign key: " + err.Error())
}
return
}
func (ss *SqlStore) RemoveIndexIfExists(indexName string, tableName string) bool {
if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES {
@@ -1121,6 +1152,20 @@ func (ss *SqlStore) RemoveIndexIfExists(indexName string, tableName string) bool
return true
}
func IsConstraintAlreadyExistsError(err error) bool {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGDuplicateObjectErrorCode {
return true
}
case *mysql.MySQLError:
if dbErr.Number == MySQLDuplicateObjectErrorCode {
return true
}
}
return false
}
func IsUniqueConstraintError(err error, indexName []string) bool {
unique := false
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" {
@@ -1197,6 +1242,10 @@ func (ss *SqlStore) Post() store.PostStore {
return ss.stores.post
}
func (ss *SqlStore) RetentionPolicy() store.RetentionPolicyStore {
return ss.stores.retentionPolicy
}
func (ss *SqlStore) User() store.UserStore {
return ss.stores.user
}

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

@@ -370,12 +370,15 @@ func (s SqlTeamStore) GetByNames(names []string) ([]*model.Team, error) {
return teams, nil
}
func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, countQuery bool) sq.SelectBuilder {
func (s SqlTeamStore) teamSearchQuery(opts *model.TeamSearch, countQuery bool) sq.SelectBuilder {
var selectStr string
if countQuery {
selectStr = "count(*)"
} else {
selectStr = "*"
selectStr = "t.*"
if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectStr += ", RetentionPoliciesTeams.PolicyId"
}
}
query := s.getQueryBuilder().
@@ -391,6 +394,7 @@ func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, count
}
}
term := opts.Term
if term != "" {
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
@@ -403,6 +407,19 @@ func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, count
query = query.Where(fmt.Sprintf("(Name %[1]s ? OR DisplayName %[1]s ?)", operatorKeyword), term, term)
}
if opts.PolicyID != nil && *opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": *opts.PolicyID})
} else if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where("RetentionPoliciesTeams.TeamId IS NULL")
} else if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId")
}
var teamFilters sq.Sqlizer
var openInviteFilter sq.Sqlizer
if opts.AllowOpenInvite != nil {
@@ -442,6 +459,11 @@ func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, count
}
}
if opts.TeamType != nil {
teamTypeFilter := sq.Eq{"Type": *opts.TeamType}
teamFilters = sq.And{teamFilters, teamTypeFilter}
}
query = query.Where(teamFilters)
return query
@@ -449,41 +471,41 @@ func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, count
// SearchAll returns from the database a list of teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, error) {
func (s SqlTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
var teams []*model.Team
queryString, args, err := s.teamSearchQuery(term, opts, false).ToSql()
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with term=%s", term)
return nil, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
return teams, nil
}
// SearchAllPaged returns a teams list and the total count of teams that matched the search.
func (s SqlTeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, error) {
func (s SqlTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
var teams []*model.Team
var totalCount int64
queryString, args, err := s.teamSearchQuery(term, opts, false).ToSql()
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, 0, errors.Wrapf(err, "failed to find Teams with term=%s", term)
return nil, 0, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
queryString, args, err = s.teamSearchQuery(term, opts, true).ToSql()
queryString, args, err = s.teamSearchQuery(opts, true).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
totalCount, err = s.GetReplica().SelectInt(queryString, args...)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count Teams with term=%s", term)
return nil, 0, errors.Wrapf(err, "failed to count Teams with term=%s", opts.Term)
}
return teams, totalCount, nil
@@ -491,53 +513,18 @@ func (s SqlTeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*mo
// SearchOpen returns from the database a list of public teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchOpen(term string) ([]*model.Team, error) {
var teams []*model.Team
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
query := s.teamsQuery.Where(sq.Eq{"Type": "O", "AllowOpenInvite": true})
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
query = query.Where(sq.Or{sq.Like{"Name": term}, sq.Like{"DisplayName": term}})
} else {
query = query.Where(sq.Or{sq.ILike{"Name": term}, sq.ILike{"DisplayName": term}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to count Teams with term=%s", term)
}
return teams, nil
func (s SqlTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(true)
return s.SearchAll(opts)
}
// SearchPrivate returns from the database a list of private teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchPrivate(term string) ([]*model.Team, error) {
var teams []*model.Team
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
query := s.teamsQuery.Where(sq.Eq{"Type": "O", "AllowOpenInvite": false})
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
query = query.Where(sq.Or{sq.Like{"Name": term}, sq.Like{"DisplayName": term}})
} else {
query = query.Where(sq.Or{sq.ILike{"Name": term}, sq.ILike{"DisplayName": term}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to count Teams with term=%s", term)
}
return teams, nil
func (s SqlTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(false)
return s.SearchAll(opts)
}
// GetAll returns all teams
@@ -558,13 +545,35 @@ func (s SqlTeamStore) GetAll() ([]*model.Team, error) {
}
// GetAllPage returns teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPage(offset int, limit int) ([]*model.Team, error) {
func (s SqlTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
var teams []*model.Team
query, args, err := s.teamsQuery.
selectString := "Teams.*"
if opts != nil && opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectString += ", RetentionPoliciesTeams.PolicyId"
}
builder := s.getQueryBuilder().
Select(selectString).
From("Teams").
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
Offset(uint64(offset))
if opts != nil {
if (opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained) ||
(opts.IncludePolicyID != nil && *opts.IncludePolicyID) {
builder = builder.LeftJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId")
}
if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
builder = builder.Where("RetentionPoliciesTeams.TeamId IS NULL")
}
if opts.AllowOpenInvite != nil {
builder = builder.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
@@ -608,43 +617,6 @@ func (s SqlTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
return data, nil
}
// GetAllPublicTeamPageListing returns public teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPublicTeamPageListing(offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var data []*model.Team
if _, err = s.GetReplica().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetAllPrivateTeamPageListing returns private teams, up to a total limit passed as paramater and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPrivateTeamPageListing(offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": false}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var data []*model.Team
if _, err = s.GetReplica().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetAllTeamListing returns all public teams.
func (s SqlTeamStore) GetAllTeamListing() ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
@@ -662,25 +634,6 @@ func (s SqlTeamStore) GetAllTeamListing() ([]*model.Team, error) {
return data, nil
}
// GetAllTeamPageListing returns public teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllTeamPageListing(offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var teams []*model.Team
if _, err = s.GetReplica().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// PermanentDelete permanently deletes from the database the team entry that matches the teamId passed as parameter.
// To soft-delete the team you can Update it with the DeleteAt field set to the current millisecond using model.GetMillis()
func (s SqlTeamStore) PermanentDelete(teamId string) error {
@@ -696,49 +649,15 @@ func (s SqlTeamStore) PermanentDelete(teamId string) error {
return nil
}
// AnalyticsPublicTeamCount returns the number of active public teams.
func (s SqlTeamStore) AnalyticsPublicTeamCount() (int64, error) {
query, args, err := s.getQueryBuilder().
Select("COUNT(*) FROM Teams").
Where(sq.Eq{"DeleteAt": 0, "AllowOpenInvite": true}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
c, err := s.GetReplica().SelectInt(query, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Teams")
}
return c, nil
}
// AnalyticsPrivateTeamCount returns the number of active private teams.
func (s SqlTeamStore) AnalyticsPrivateTeamCount() (int64, error) {
query, args, err := s.getQueryBuilder().
Select("COUNT(*) FROM Teams").
Where(sq.Eq{"DeleteAt": 0, "AllowOpenInvite": false}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
c, err := s.GetReplica().SelectInt(query, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Teams")
}
return c, nil
}
// AnalyticsTeamCount returns the total number of teams including deleted teams if parameter passed is set to 'true'.
func (s SqlTeamStore) AnalyticsTeamCount(includeDeleted bool) (int64, error) {
// AnalyticsTeamCount returns the total number of teams.
func (s SqlTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
query := s.getQueryBuilder().Select("COUNT(*) FROM Teams")
if !includeDeleted {
if opts == nil || (opts.IncludeDeleted != nil && !*opts.IncludeDeleted) {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts != nil && opts.AllowOpenInvite != nil {
query = query.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
queryString, args, err := query.ToSql()
if err != nil {