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

  1. Log unionSQL when this fires and look for literal '?' characters that are not placeholders (e.g. inside string literals or json operators).
  2. Escape or remove literal '?' from injected SQL fragments.
  3. Update the squirrel dependency to a current version.
  4. 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

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


AI-assisted analysis of mattermost-community/focalboard@a84bbb65e3 (2026-08-30). Data as JSON: /api/errors/6ef583c545de71b8. Report an issue: GitHub.