mattermost-community/focalboard · error
SearchBoardsForUser unable to replace unionSQL placeholders:
Error message
SearchBoardsForUser unable to replace unionSQL placeholders: %w
What it means
After building the union SQL with '?' placeholders, the code rewrites placeholders to Postgres/SQLite '$N' form using sq.Dollar.ReplacePlaceholders(unionSQL). This error means that string rewriting failed — rare, since it operates purely on the rendered SQL string.
Source
Thrown at server/services/store/mattermostauthlayer/mattermostauthlayer.go:784
} else if includePublicBoards {
unionQ = unionQ.
Prefix("(").
Suffix(") UNION ("+teamMembersSQL+")", teamMembersArgs...)
}
unionSQL, unionArgs, err := unionQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUser error getting unionSQL: %w", err)
}
// if we're using postgres or sqlite, we need to replace the
// question mark placeholder with the numbered dollar one, now
// that the full query is built
if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType {
var rErr error
unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL)
if rErr != nil {
return nil, fmt.Errorf("SearchBoardsForUser unable to replace unionSQL placeholders: %w", rErr)
}
}
rows, err := s.mmDB.Query(unionSQL, unionArgs...)
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows, false)
}
// searchBoardsForUserInTeam returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *MattermostAuthLayer) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {View on GitHub (pinned to a84bbb65e3)
Solutions
- Log unionSQL when this fires and look for literal '?' characters that are not placeholders (e.g. inside string literals or json operators).
- Escape or remove literal '?' from injected SQL fragments.
- Update the squirrel dependency to a current version.
- If only MySQL is intended, verify s.dbType is detected correctly so Postgres/SQLite branch isn't taken wrongly.
Example fix
// before
unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL)
if rErr != nil {
return nil, fmt.Errorf("SearchBoardsForUser unable to replace unionSQL placeholders: %w", rErr)
}
// after
// ensure injected fragments don't contain raw '?'; log for diagnosis
s.logger.Error("placeholder rewrite failed", mlog.String("sql", unionSQL), mlog.Err(rErr))
unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL)
if rErr != nil {
return nil, fmt.Errorf("SearchBoardsForUser unable to replace unionSQL placeholders: %w", rErr)
} Defensive patterns
Strategy: try-catch
Try / catch
boards, err := store.SearchBoardsForUser(userID, term)
if err != nil {
if strings.Contains(err.Error(), "unable to replace unionSQL placeholders") {
logger.Error("placeholder rewrite failed; check for literal '?' in SQL fragments", "err", err)
return nil, ErrInternalSearch
}
return nil, err
} Prevention
- Avoid literal '?' characters in injected SQL fragments.
- Test queries against both MySQL and Postgres/SQLite dialects.
- Keep squirrel up to date.
When it happens
Trigger: s.dbType is postgres or sqlite and ReplacePlaceholders fails on the union SQL string — essentially only when the rendered SQL is malformed (e.g. a stray '?' or broken string from a bug in earlier builder steps).
Common situations: Custom SQL fragments injected via Suffix containing '?' characters that aren't placeholders; corrupted SQL from earlier build steps; very old squirrel versions with escaping bugs.
Related errors
- SearchBoardsForUserInTeam unable to replace unionSQL placeho
- getBlockHistoryNewestChildren unable to replace sql placehol
- SearchBoardsForUser error getting teamMembersSQL: %w
- SearchBoardsForUser error getting channelMembersSQL: %w
- SearchBoardsForUser error getting unionSQL: %w
AI-assisted analysis of mattermost-community/focalboard@a84bbb65e3 (2026-08-30).
Data as JSON: /api/errors/6ef583c545de71b8.
Report an issue: GitHub.