fleet/server/platform/mysql/list_options.go
Victor Lyuboslavsky 506901443d
Moved common_mysql package to server/platform/mysql (#38017)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #37244

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] QA'd all new/changed functionality manually



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Internal MySQL utility package reorganized and all internal imports
updated to the new platform location; no changes to end-user
functionality or behavior.

* **Documentation**
* Added platform package documentation describing infrastructure
responsibilities and architectural boundaries to guide maintainers.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-08 13:17:19 -06:00

103 lines
2.9 KiB
Go

package mysql
import (
"fmt"
"regexp"
"strings"
)
// columnCharsRegexp matches characters that are not allowed in column names.
var columnCharsRegexp = regexp.MustCompile(`[^\w-.]`)
// ListOptions defines the interface for pagination and sorting options.
// This interface allows the common_mysql package to work with various list options implementations.
type ListOptions interface {
GetPage() uint
GetPerPage() uint
GetOrderKey() string
IsDescending() bool
GetCursorValue() string
WantsPaginationInfo() bool
GetSecondaryOrderKey() string
IsSecondaryDescending() bool
}
// SanitizeColumn sanitizes a column name for use in SQL queries.
// It removes invalid characters and wraps parts in backticks.
func SanitizeColumn(col string) string {
col = columnCharsRegexp.ReplaceAllString(col, "")
oldParts := strings.Split(col, ".")
parts := oldParts[:0]
for _, p := range oldParts {
if len(p) != 0 {
parts = append(parts, p)
}
}
if len(parts) == 0 {
return ""
}
col = "`" + strings.Join(parts, "`.`") + "`"
return col
}
// AppendListOptions appends ORDER BY, LIMIT, and OFFSET clauses to a SQL string
// based on the provided list options.
func AppendListOptions(sql string, opts ListOptions) (string, []any) {
return AppendListOptionsWithParams(sql, nil, opts)
}
// AppendListOptionsWithParams appends ORDER BY, LIMIT, and OFFSET clauses to a SQL string
// based on the provided list options. It accepts existing query params and returns
// the extended params slice.
func AppendListOptionsWithParams(sql string, params []any, opts ListOptions) (string, []any) {
orderKey := SanitizeColumn(opts.GetOrderKey())
page := opts.GetPage()
if cursor := opts.GetCursorValue(); cursor != "" && orderKey != "" {
cursorSQL := " WHERE "
if strings.Contains(strings.ToLower(sql), "where") {
cursorSQL = " AND "
}
// Cursor value is always passed as string. MySQL automatically converts
// string to integer when comparing against integer columns.
// See: https://dev.mysql.com/doc/refman/8.0/en/type-conversion.html
params = append(params, cursor)
direction := ">" // ASC
if opts.IsDescending() {
direction = "<" // DESC
}
sql = fmt.Sprintf("%s %s %s %s ?", sql, cursorSQL, orderKey, direction)
// Cursor-based pagination supersedes page-based pagination
page = 0
}
if orderKey != "" {
direction := "ASC"
if opts.IsDescending() {
direction = "DESC"
}
sql = fmt.Sprintf("%s ORDER BY %s %s", sql, orderKey, direction)
if opts.GetSecondaryOrderKey() != "" {
dir := "ASC"
if opts.IsSecondaryDescending() {
dir = "DESC"
}
sql += fmt.Sprintf(`, %s %s`, SanitizeColumn(opts.GetSecondaryOrderKey()), dir)
}
}
limit := opts.GetPerPage()
if opts.WantsPaginationInfo() {
limit++
}
sql = fmt.Sprintf("%s LIMIT %d", sql, limit)
offset := opts.GetPerPage() * page
if offset > 0 {
sql = fmt.Sprintf("%s OFFSET %d", sql, offset)
}
return sql, params
}