You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
themis/fmt.go

116 lines
2.6 KiB

package themis
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
ctx, span := tracer.Start(ctx, "format_rows", trace.WithAttributes(
semconv.DBSystemSqlite,
))
defer span.End()
sb := strings.Builder{}
cols, err := rows.Columns()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get rows columns")
return "", fmt.Errorf("failed to get rows columns: %w", err)
}
log.Debug().Ctx(ctx).Int("columns", len(cols)).Msg("formatting sql rows to markdown table")
c := make([]string, len(cols))
for i := range c {
c[i] = " %-*s "
}
pattern := fmt.Sprintf("|%s|\n", strings.Join(c, "|"))
lengths := make([]int, len(cols))
for i := range lengths {
lengths[i] = len(cols[i])
}
scanned := make([][]any, 0)
for rows.Next() {
row := make([]interface{}, len(cols))
for i := range row {
row[i] = new(sql.NullString)
}
if err := rows.Scan(row...); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return "", fmt.Errorf("failed to scan next row: %w", err)
}
scanned = append(scanned, row) // keep track of row for later
for i, a := range row {
s := a.(*sql.NullString)
if len(s.String) > lengths[i] {
lengths[i] = len(s.String)
}
}
}
log.Debug().Ctx(ctx).Int("rows", len(scanned)).Ints("lengths", lengths).Msg("scanned rows and extracted max column lengths")
// Write column names
curr := make([]any, 0, 2*len(cols))
for i := range lengths {
curr = append(curr, lengths[i], cols[i])
}
sb.WriteString(fmt.Sprintf(pattern, curr...))
// Write header separator row
curr = curr[:0] // empty slice but preserve capacity
for i := range lengths {
curr = append(curr, lengths[i], strings.Repeat("-", lengths[i]))
}
sb.WriteString(fmt.Sprintf(pattern, curr...))
// iterate rows and write each one
for _, r := range scanned {
curr = curr[:0] // empty slice but preserve capacity
for i := range lengths {
s := r[i].(*sql.NullString)
curr = append(curr, lengths[i], s.String)
}
sb.WriteString(fmt.Sprintf(pattern, curr...))
}
return sb.String(), nil
}
func FormatStringSlice(s []string) string {
if len(s) == 0 {
return ""
}
sb := strings.Builder{}
for len(s) > 0 {
curr, rest := s[0], s[1:]
sb.WriteString(curr)
if len(rest) == 0 {
break
}
if len(rest) > 1 {
sb.WriteString(", ")
} else {
sb.WriteString(" and ")
}
s = rest
}
return sb.String()
}