284 lines
5.9 KiB
Go
284 lines
5.9 KiB
Go
package execute
|
|
|
|
import (
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/influxdata/platform/query"
|
|
)
|
|
|
|
const fixedWidthTimeFmt = "2006-01-02T15:04:05.000000000Z"
|
|
|
|
// Formatter writes a table to a Writer.
|
|
type Formatter struct {
|
|
tbl query.Table
|
|
widths []int
|
|
maxWidth int
|
|
newWidths []int
|
|
pad []byte
|
|
dash []byte
|
|
// fmtBuf is used to format values
|
|
fmtBuf [64]byte
|
|
|
|
opts FormatOptions
|
|
|
|
cols orderedCols
|
|
}
|
|
type FormatOptions struct {
|
|
// RepeatHeaderCount is the number of rows to print before printing the header again.
|
|
// If zero then the headers are not repeated.
|
|
RepeatHeaderCount int
|
|
}
|
|
|
|
func DefaultFormatOptions() *FormatOptions {
|
|
return &FormatOptions{}
|
|
}
|
|
|
|
var eol = []byte{'\n'}
|
|
|
|
// NewFormatter creates a Formatter for a given table.
|
|
// If opts is nil, the DefaultFormatOptions are used.
|
|
func NewFormatter(tbl query.Table, opts *FormatOptions) *Formatter {
|
|
if opts == nil {
|
|
opts = DefaultFormatOptions()
|
|
}
|
|
return &Formatter{
|
|
tbl: tbl,
|
|
opts: *opts,
|
|
}
|
|
}
|
|
|
|
type writeToHelper struct {
|
|
w io.Writer
|
|
n int64
|
|
err error
|
|
}
|
|
|
|
func (w *writeToHelper) write(data []byte) {
|
|
if w.err != nil {
|
|
return
|
|
}
|
|
n, err := w.w.Write(data)
|
|
w.n += int64(n)
|
|
w.err = err
|
|
}
|
|
|
|
var minWidthsByType = map[query.DataType]int{
|
|
query.TBool: 12,
|
|
query.TInt: 26,
|
|
query.TUInt: 27,
|
|
query.TFloat: 28,
|
|
query.TString: 22,
|
|
query.TTime: len(fixedWidthTimeFmt),
|
|
query.TInvalid: 10,
|
|
}
|
|
|
|
// WriteTo writes the formatted table data to w.
|
|
func (f *Formatter) WriteTo(out io.Writer) (int64, error) {
|
|
w := &writeToHelper{w: out}
|
|
|
|
// Sort cols
|
|
cols := f.tbl.Cols()
|
|
f.cols = newOrderedCols(cols, f.tbl.Key())
|
|
sort.Sort(f.cols)
|
|
|
|
// Compute header widths
|
|
f.widths = make([]int, len(cols))
|
|
for j, c := range cols {
|
|
l := len(c.Label)
|
|
min := minWidthsByType[c.Type]
|
|
if min > l {
|
|
l = min
|
|
}
|
|
if l > f.widths[j] {
|
|
f.widths[j] = l
|
|
}
|
|
if l > f.maxWidth {
|
|
f.maxWidth = l
|
|
}
|
|
}
|
|
|
|
// Write table header
|
|
w.write([]byte("Table: keys: ["))
|
|
labels := make([]string, len(f.tbl.Key().Cols()))
|
|
for i, c := range f.tbl.Key().Cols() {
|
|
labels[i] = c.Label
|
|
}
|
|
w.write([]byte(strings.Join(labels, ", ")))
|
|
w.write([]byte("]"))
|
|
w.write(eol)
|
|
|
|
// Check err and return early
|
|
if w.err != nil {
|
|
return w.n, w.err
|
|
}
|
|
|
|
// Write rows
|
|
r := 0
|
|
w.err = f.tbl.Do(func(cr query.ColReader) error {
|
|
if r == 0 {
|
|
l := cr.Len()
|
|
for i := 0; i < l; i++ {
|
|
for oj, c := range f.cols.cols {
|
|
j := f.cols.Idx(oj)
|
|
buf := f.valueBuf(i, j, c.Type, cr)
|
|
l := len(buf)
|
|
if l > f.widths[j] {
|
|
f.widths[j] = l
|
|
}
|
|
if l > f.maxWidth {
|
|
f.maxWidth = l
|
|
}
|
|
}
|
|
}
|
|
f.makePaddingBuffers()
|
|
f.writeHeader(w)
|
|
f.writeHeaderSeparator(w)
|
|
f.newWidths = make([]int, len(f.widths))
|
|
copy(f.newWidths, f.widths)
|
|
}
|
|
l := cr.Len()
|
|
for i := 0; i < l; i++ {
|
|
for oj, c := range f.cols.cols {
|
|
j := f.cols.Idx(oj)
|
|
buf := f.valueBuf(i, j, c.Type, cr)
|
|
l := len(buf)
|
|
padding := f.widths[j] - l
|
|
if padding >= 0 {
|
|
w.write(f.pad[:padding])
|
|
w.write(buf)
|
|
} else {
|
|
//TODO make unicode friendly
|
|
w.write(buf[:f.widths[j]-3])
|
|
w.write([]byte{'.', '.', '.'})
|
|
}
|
|
w.write(f.pad[:2])
|
|
if l > f.newWidths[j] {
|
|
f.newWidths[j] = l
|
|
}
|
|
if l > f.maxWidth {
|
|
f.maxWidth = l
|
|
}
|
|
}
|
|
w.write(eol)
|
|
r++
|
|
if f.opts.RepeatHeaderCount > 0 && r%f.opts.RepeatHeaderCount == 0 {
|
|
copy(f.widths, f.newWidths)
|
|
f.makePaddingBuffers()
|
|
f.writeHeaderSeparator(w)
|
|
f.writeHeader(w)
|
|
f.writeHeaderSeparator(w)
|
|
}
|
|
}
|
|
return w.err
|
|
})
|
|
return w.n, w.err
|
|
}
|
|
|
|
func (f *Formatter) makePaddingBuffers() {
|
|
if len(f.pad) != f.maxWidth {
|
|
f.pad = make([]byte, f.maxWidth)
|
|
for i := range f.pad {
|
|
f.pad[i] = ' '
|
|
}
|
|
}
|
|
if len(f.dash) != f.maxWidth {
|
|
f.dash = make([]byte, f.maxWidth)
|
|
for i := range f.dash {
|
|
f.dash[i] = '-'
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *Formatter) writeHeader(w *writeToHelper) {
|
|
for oj, c := range f.cols.cols {
|
|
j := f.cols.Idx(oj)
|
|
buf := append(append([]byte(c.Label), ':'), []byte(c.Type.String())...)
|
|
w.write(f.pad[:f.widths[j]-len(buf)])
|
|
w.write(buf)
|
|
w.write(f.pad[:2])
|
|
}
|
|
w.write(eol)
|
|
}
|
|
|
|
func (f *Formatter) writeHeaderSeparator(w *writeToHelper) {
|
|
for oj := range f.cols.cols {
|
|
j := f.cols.Idx(oj)
|
|
w.write(f.dash[:f.widths[j]])
|
|
w.write(f.pad[:2])
|
|
}
|
|
w.write(eol)
|
|
}
|
|
|
|
func (f *Formatter) valueBuf(i, j int, typ query.DataType, cr query.ColReader) (buf []byte) {
|
|
switch typ {
|
|
case query.TBool:
|
|
buf = strconv.AppendBool(f.fmtBuf[0:0], cr.Bools(j)[i])
|
|
case query.TInt:
|
|
buf = strconv.AppendInt(f.fmtBuf[0:0], cr.Ints(j)[i], 10)
|
|
case query.TUInt:
|
|
buf = strconv.AppendUint(f.fmtBuf[0:0], cr.UInts(j)[i], 10)
|
|
case query.TFloat:
|
|
// TODO allow specifying format and precision
|
|
buf = strconv.AppendFloat(f.fmtBuf[0:0], cr.Floats(j)[i], 'f', -1, 64)
|
|
case query.TString:
|
|
buf = []byte(cr.Strings(j)[i])
|
|
case query.TTime:
|
|
buf = []byte(cr.Times(j)[i].String())
|
|
}
|
|
return
|
|
}
|
|
|
|
// orderedCols sorts a list of columns:
|
|
//
|
|
// * time
|
|
// * common tags sorted by label
|
|
// * other tags sorted by label
|
|
// * value
|
|
//
|
|
type orderedCols struct {
|
|
indexMap []int
|
|
cols []query.ColMeta
|
|
key query.GroupKey
|
|
}
|
|
|
|
func newOrderedCols(cols []query.ColMeta, key query.GroupKey) orderedCols {
|
|
indexMap := make([]int, len(cols))
|
|
for i := range indexMap {
|
|
indexMap[i] = i
|
|
}
|
|
cpy := make([]query.ColMeta, len(cols))
|
|
copy(cpy, cols)
|
|
return orderedCols{
|
|
indexMap: indexMap,
|
|
cols: cpy,
|
|
key: key,
|
|
}
|
|
}
|
|
|
|
func (o orderedCols) Idx(oj int) int {
|
|
return o.indexMap[oj]
|
|
}
|
|
|
|
func (o orderedCols) Len() int { return len(o.cols) }
|
|
func (o orderedCols) Swap(i int, j int) {
|
|
o.cols[i], o.cols[j] = o.cols[j], o.cols[i]
|
|
o.indexMap[i], o.indexMap[j] = o.indexMap[j], o.indexMap[i]
|
|
}
|
|
|
|
func (o orderedCols) Less(i int, j int) bool {
|
|
ki := ColIdx(o.cols[i].Label, o.key.Cols())
|
|
kj := ColIdx(o.cols[j].Label, o.key.Cols())
|
|
if ki >= 0 && kj >= 0 {
|
|
return ki < kj
|
|
} else if ki >= 0 {
|
|
return true
|
|
} else if kj >= 0 {
|
|
return false
|
|
}
|
|
|
|
return i < j
|
|
}
|