fix(influxql): set correct Content-Type on v1 query responses (#20565)
parent
52da4020f8
commit
10fcc2bf1d
|
@ -54,6 +54,8 @@ Replacement `tsi1` indexes will be automatically generated on startup for shards
|
||||||
1. [20527](https://github.com/influxdata/influxdb/pull/20527): Allow backups to complete while a snapshot is in progress.
|
1. [20527](https://github.com/influxdata/influxdb/pull/20527): Allow backups to complete while a snapshot is in progress.
|
||||||
1. [20539](https://github.com/influxdata/influxdb/pull/20539): Prevent extra output row from GROUP BY crossing DST boundary.
|
1. [20539](https://github.com/influxdata/influxdb/pull/20539): Prevent extra output row from GROUP BY crossing DST boundary.
|
||||||
1. [20548](https://github.com/influxdata/influxdb/pull/20548): Prevent panic in `influxd upgrade` when V1 users exist and no V1 config is given.
|
1. [20548](https://github.com/influxdata/influxdb/pull/20548): Prevent panic in `influxd upgrade` when V1 users exist and no V1 config is given.
|
||||||
|
1. [20565](https://github.com/influxdata/influxdb/pull/20565): Set correct Content-Type on v1 query responses.
|
||||||
|
1. [20565](https://github.com/influxdata/influxdb/pull/20565): Update V1 API spec to document all valid Accept headers and matching Content-Types.
|
||||||
|
|
||||||
## v2.0.3 [2020-12-14]
|
## v2.0.3 [2020-12-14]
|
||||||
|
|
||||||
|
|
|
@ -142,11 +142,15 @@ func (h *InfluxqlHandler) handleInfluxqldQuery(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatString := r.Header.Get("Accept")
|
||||||
|
encodingFormat := influxql.EncodingFormatFromMimeType(formatString)
|
||||||
|
w.Header().Set("Content-Type", encodingFormat.ContentType())
|
||||||
|
|
||||||
req := &influxql.QueryRequest{
|
req := &influxql.QueryRequest{
|
||||||
DB: r.FormValue("db"),
|
DB: r.FormValue("db"),
|
||||||
RP: r.FormValue("rp"),
|
RP: r.FormValue("rp"),
|
||||||
Epoch: r.FormValue("epoch"),
|
Epoch: r.FormValue("epoch"),
|
||||||
EncodingFormat: influxql.EncodingFormatFromMimeType(r.Header.Get("Accept")),
|
EncodingFormat: encodingFormat,
|
||||||
OrganizationID: o.ID,
|
OrganizationID: o.ID,
|
||||||
Query: query,
|
Query: query,
|
||||||
Params: params,
|
Params: params,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
imock "github.com/influxdata/influxdb/v2/influxql/mock"
|
imock "github.com/influxdata/influxdb/v2/influxql/mock"
|
||||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||||
"github.com/influxdata/influxdb/v2/mock"
|
"github.com/influxdata/influxdb/v2/mock"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmpOpts = []cmp.Option{
|
var cmpOpts = []cmp.Option{
|
||||||
|
@ -41,8 +42,6 @@ var cmpOpts = []cmp.Option{
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
||||||
t.Skip("almost good to go, only unexpected content types")
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
|
@ -61,7 +60,6 @@ func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
||||||
wantCode int
|
wantCode int
|
||||||
wantHeader http.Header
|
wantHeader http.Header
|
||||||
wantBody []byte
|
wantBody []byte
|
||||||
wantLogs []string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no token causes http error",
|
name: "no token causes http error",
|
||||||
|
@ -189,15 +187,14 @@ func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
r: WithHeader(httptest.NewRequest("POST", "/query", nil).WithContext(ctx), "Accept", "text/csv"),
|
r: WithHeader(httptest.NewRequest("POST", "/query", nil).WithContext(ctx), "Accept", "application/foo"),
|
||||||
w: httptest.NewRecorder(),
|
w: httptest.NewRecorder(),
|
||||||
},
|
},
|
||||||
wantBody: []byte("good"),
|
wantBody: []byte("good"),
|
||||||
wantCode: http.StatusOK,
|
wantCode: http.StatusOK,
|
||||||
wantHeader: http.Header{
|
wantHeader: http.Header{
|
||||||
"Content-Type": {"text/csv"},
|
"Content-Type": {"application/json"},
|
||||||
},
|
},
|
||||||
wantLogs: []string{"text/csv"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good query",
|
name: "good query",
|
||||||
|
@ -235,6 +232,7 @@ func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
h := NewInfluxQLHandler(b, HandlerConfig{})
|
h := NewInfluxQLHandler(b, HandlerConfig{})
|
||||||
|
h.Logger = zaptest.NewLogger(t)
|
||||||
|
|
||||||
if tt.context != nil {
|
if tt.context != nil {
|
||||||
tt.args.r = tt.args.r.WithContext(tt.context)
|
tt.args.r = tt.args.r.WithContext(tt.context)
|
||||||
|
@ -255,7 +253,6 @@ func TestInfluxQLdHandler_HandleQuery(t *testing.T) {
|
||||||
if got, want := tt.args.w.Body.Bytes(), tt.wantBody; !cmp.Equal(got, want) {
|
if got, want := tt.args.w.Body.Bytes(), tt.wantBody; !cmp.Equal(got, want) {
|
||||||
t.Errorf("HandleQuery() body = got(-)/want(+) %s", cmp.Diff(string(got), string(want)))
|
t.Errorf("HandleQuery() body = got(-)/want(+) %s", cmp.Diff(string(got), string(want)))
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,17 @@ paths:
|
||||||
- $ref: "#/components/parameters/TraceSpan"
|
- $ref: "#/components/parameters/TraceSpan"
|
||||||
- $ref: "#/components/parameters/AuthUserV1"
|
- $ref: "#/components/parameters/AuthUserV1"
|
||||||
- $ref: "#/components/parameters/AuthPassV1"
|
- $ref: "#/components/parameters/AuthPassV1"
|
||||||
|
- in: header
|
||||||
|
name: Accept
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Specifies how query results should be encoded in the response.
|
||||||
|
default: application/json
|
||||||
|
enum:
|
||||||
|
- application/json
|
||||||
|
- application/csv
|
||||||
|
- text/csv
|
||||||
|
- application/x-msgpack
|
||||||
- in: header
|
- in: header
|
||||||
name: Accept-Encoding
|
name: Accept-Encoding
|
||||||
description: The Accept-Encoding request HTTP header advertises which content encoding, usually a compression algorithm, the client is able to understand.
|
description: The Accept-Encoding request HTTP header advertises which content encoding, usually a compression algorithm, the client is able to understand.
|
||||||
|
@ -154,17 +165,19 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: Specifies the request's trace ID.
|
description: Specifies the request's trace ID.
|
||||||
content:
|
content:
|
||||||
|
application/csv:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/InfluxQLCSVResponse"
|
||||||
text/csv:
|
text/csv:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
$ref: "#/components/schemas/InfluxQLCSVResponse"
|
||||||
example: >
|
application/json:
|
||||||
name,tags,time,test_field,test_tag
|
|
||||||
test_measurement,,1603740794286107366,1,tag_value
|
|
||||||
test_measurement,,1603740870053205649,2,tag_value
|
|
||||||
test_measurement,,1603741221085428881,3,tag_value
|
|
||||||
text/plain:
|
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/InfluxQLResponse"
|
$ref: "#/components/schemas/InfluxQLResponse"
|
||||||
|
application/x-msgpack:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
"429":
|
"429":
|
||||||
description: Token is temporarily over quota. The Retry-After header describes when to try the read again.
|
description: Token is temporarily over quota. The Retry-After header describes when to try the read again.
|
||||||
headers:
|
headers:
|
||||||
|
@ -233,6 +246,14 @@ components:
|
||||||
items:
|
items:
|
||||||
type: array
|
type: array
|
||||||
items: {}
|
items: {}
|
||||||
|
InfluxQLCSVResponse:
|
||||||
|
type: string
|
||||||
|
example: >
|
||||||
|
name,tags,time,test_field,test_tag
|
||||||
|
test_measurement,,1603740794286107366,1,tag_value
|
||||||
|
test_measurement,,1603740870053205649,2,tag_value
|
||||||
|
test_measurement,,1603741221085428881,3,tag_value
|
||||||
|
|
||||||
Error:
|
Error:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
|
|
|
@ -74,6 +74,7 @@ func (q *Query) Execute(ctx context.Context, t *testing.T, db string, c *tests.C
|
||||||
QueryParams(params...).
|
QueryParams(params...).
|
||||||
Header("Accept", "application/json").
|
Header("Accept", "application/json").
|
||||||
RespFn(func(resp *http.Response) error {
|
RespFn(func(resp *http.Response) error {
|
||||||
|
require.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
q.got = strings.TrimSpace(string(b))
|
q.got = strings.TrimSpace(string(b))
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -5,10 +5,8 @@ description: |
|
||||||
tests:
|
tests:
|
||||||
- query: "select sum(f0) from m0"
|
- query: "select sum(f0) from m0"
|
||||||
result: |
|
result: |
|
||||||
name: m0
|
name,tags,time,sum
|
||||||
time sum
|
m0,,0,10
|
||||||
---- ---
|
|
||||||
0 10
|
|
||||||
|
|
||||||
generated:
|
generated:
|
||||||
start: "1000000000000"
|
start: "1000000000000"
|
||||||
|
|
|
@ -8,10 +8,8 @@ tests:
|
||||||
- name: "no_aggregate"
|
- name: "no_aggregate"
|
||||||
query: "select f1 from m0 where f0 > 1 AND time >= 30000000000 AND time <= 50000000000"
|
query: "select f1 from m0 where f0 > 1 AND time >= 30000000000 AND time <= 50000000000"
|
||||||
result: |
|
result: |
|
||||||
name: m0
|
name,tags,time,f1
|
||||||
time f1
|
m0,,50000000000,30
|
||||||
---- --
|
|
||||||
50000000000 30
|
|
||||||
|
|
||||||
- name: "aggregate"
|
- name: "aggregate"
|
||||||
description: |
|
description: |
|
||||||
|
@ -19,10 +17,8 @@ tests:
|
||||||
|
|
||||||
query: "SELECT sum(f1) FROM m0 WHERE f0 >= 1"
|
query: "SELECT sum(f1) FROM m0 WHERE f0 >= 1"
|
||||||
result: |
|
result: |
|
||||||
name: m0
|
name,tags,time,sum
|
||||||
time sum
|
m0,,0,50
|
||||||
---- ---
|
|
||||||
0 50
|
|
||||||
|
|
||||||
dataset: |
|
dataset: |
|
||||||
m0,t0=tv0 f0=0i,f1=10i 30000000000
|
m0,t0=tv0 f0=0i,f1=10i 30000000000
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
tests:
|
tests:
|
||||||
- query: "select sum(used), mean(active) from mem where time >= 30000000000 AND time < 50000000000"
|
- query: "select sum(used), mean(active) from mem where time >= 30000000000 AND time < 50000000000"
|
||||||
result: |
|
result: |
|
||||||
name: mem
|
name,tags,time,sum,mean
|
||||||
time sum mean
|
mem,,30000000000,6549852160,7164311552
|
||||||
---- --- ----
|
|
||||||
30000000000 6549852160 7164311552
|
|
||||||
|
|
||||||
dataset: |
|
dataset: |
|
||||||
mem,host=gianarb page_tables=39534592i,vmalloc_chunk=0i,write_back_tmp=0i,dirty=884736i,high_total=0i,available=11992494080i,used=3284553728i,active=7172775936i,huge_pages_free=0i,swap_total=8589930496i,vmalloc_used=38604800i,free=4928421888i,commit_limit=16853958656i,committed_as=12584218624i,mapped=939278336i,vmalloc_total=35184372087808i,write_back=0i,buffered=989163520i,wired=0i,low_free=0i,huge_page_size=2097152i,swap_cached=120016896i,swap_free=8445227008i,inactive=3461185536i,slab=542363648i,high_free=0i,shared=903233536i,sreclaimable=449650688i,total=16528056320i,cached=7325917184i,available_percent=72.55840522208482,sunreclaim=92712960i,used_percent=19.87259520664557,huge_pages_total=0i,low_total=0i 0
|
mem,host=gianarb page_tables=39534592i,vmalloc_chunk=0i,write_back_tmp=0i,dirty=884736i,high_total=0i,available=11992494080i,used=3284553728i,active=7172775936i,huge_pages_free=0i,swap_total=8589930496i,vmalloc_used=38604800i,free=4928421888i,commit_limit=16853958656i,committed_as=12584218624i,mapped=939278336i,vmalloc_total=35184372087808i,write_back=0i,buffered=989163520i,wired=0i,low_free=0i,huge_page_size=2097152i,swap_cached=120016896i,swap_free=8445227008i,inactive=3461185536i,slab=542363648i,high_free=0i,shared=903233536i,sreclaimable=449650688i,total=16528056320i,cached=7325917184i,available_percent=72.55840522208482,sunreclaim=92712960i,used_percent=19.87259520664557,huge_pages_total=0i,low_total=0i 0
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
tests:
|
tests:
|
||||||
- query: "select host, inactive from mem where time >=30000000000 AND time < 50000000000"
|
- query: "select host, inactive from mem where time >=30000000000 AND time < 50000000000"
|
||||||
result: |
|
result: |
|
||||||
name: mem
|
name,tags,time,host,inactive
|
||||||
time host inactive
|
mem,,30000000000,gianarb,3460194304
|
||||||
---- ---- --------
|
mem,,40000000000,gianarb,3454791680
|
||||||
30000000000 gianarb 3460194304
|
|
||||||
40000000000 gianarb 3454791680
|
|
||||||
|
|
||||||
dataset: |
|
dataset: |
|
||||||
mem,host=gianarb page_tables=39534592i,vmalloc_chunk=0i,write_back_tmp=0i,dirty=884736i,high_total=0i,available=11992494080i,used=3284553728i,active=7172775936i,huge_pages_free=0i,swap_total=8589930496i,vmalloc_used=38604800i,free=4928421888i,commit_limit=16853958656i,committed_as=12584218624i,mapped=939278336i,vmalloc_total=35184372087808i,write_back=0i,buffered=989163520i,wired=0i,low_free=0i,huge_page_size=2097152i,swap_cached=120016896i,swap_free=8445227008i,inactive=3461185536i,slab=542363648i,high_free=0i,shared=903233536i,sreclaimable=449650688i,total=16528056320i,cached=7325917184i,available_percent=72.55840522208482,sunreclaim=92712960i,used_percent=19.87259520664557,huge_pages_total=0i,low_total=0i 0
|
mem,host=gianarb page_tables=39534592i,vmalloc_chunk=0i,write_back_tmp=0i,dirty=884736i,high_total=0i,available=11992494080i,used=3284553728i,active=7172775936i,huge_pages_free=0i,swap_total=8589930496i,vmalloc_used=38604800i,free=4928421888i,commit_limit=16853958656i,committed_as=12584218624i,mapped=939278336i,vmalloc_total=35184372087808i,write_back=0i,buffered=989163520i,wired=0i,low_free=0i,huge_page_size=2097152i,swap_cached=120016896i,swap_free=8445227008i,inactive=3461185536i,slab=542363648i,high_free=0i,shared=903233536i,sreclaimable=449650688i,total=16528056320i,cached=7325917184i,available_percent=72.55840522208482,sunreclaim=92712960i,used_percent=19.87259520664557,huge_pages_total=0i,low_total=0i 0
|
||||||
|
|
|
@ -3,11 +3,9 @@ tests:
|
||||||
billing:
|
billing:
|
||||||
point_count: 1
|
point_count: 1
|
||||||
result: |
|
result: |
|
||||||
name: m0
|
name,tags,time,f0,f1,t0
|
||||||
time f0 f1 t0
|
m0,,30000000000,0,10,tv0
|
||||||
---- -- -- --
|
m0,,40000000000,1,20,tv1
|
||||||
30000000000 0 10 tv0
|
|
||||||
40000000000 1 20 tv1
|
|
||||||
|
|
||||||
dataset: |
|
dataset: |
|
||||||
m0,t0=tv0 f0=0i,f1=10i 30000000000
|
m0,t0=tv0 f0=0i,f1=10i 30000000000
|
||||||
|
|
|
@ -64,10 +64,10 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"result": {
|
"result": {
|
||||||
"description": "The expected results in table format",
|
"description": "The expected results in CSV format",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,7 +176,7 @@ func validate(t *testing.T, gf *TestSuite) {
|
||||||
QueryParams([2]string{"q", test.Query}).
|
QueryParams([2]string{"q", test.Query}).
|
||||||
QueryParams([2]string{"epoch", "ns"}).
|
QueryParams([2]string{"epoch", "ns"}).
|
||||||
Header("Content-Type", "application/vnd.influxql").
|
Header("Content-Type", "application/vnd.influxql").
|
||||||
Header("Accept", "text/plain").
|
Header("Accept", "application/csv").
|
||||||
RespFn(func(resp *http.Response) error {
|
RespFn(func(resp *http.Response) error {
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -6,13 +6,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/influxql"
|
"github.com/influxdata/influxdb/v2/influxql"
|
||||||
|
@ -31,10 +26,8 @@ type ResponseWriter interface {
|
||||||
// in the request that wraps the ResponseWriter.
|
// in the request that wraps the ResponseWriter.
|
||||||
func NewResponseWriter(encoding influxql.EncodingFormat) ResponseWriter {
|
func NewResponseWriter(encoding influxql.EncodingFormat) ResponseWriter {
|
||||||
switch encoding {
|
switch encoding {
|
||||||
case influxql.EncodingFormatCSV:
|
case influxql.EncodingFormatAppCSV, influxql.EncodingFormatTextCSV:
|
||||||
return &csvFormatter{statementID: -1}
|
return &csvFormatter{statementID: -1}
|
||||||
case influxql.EncodingFormatTable:
|
|
||||||
return &textFormatter{}
|
|
||||||
case influxql.EncodingFormatMessagePack:
|
case influxql.EncodingFormatMessagePack:
|
||||||
return &msgpFormatter{}
|
return &msgpFormatter{}
|
||||||
case influxql.EncodingFormatJSON:
|
case influxql.EncodingFormatJSON:
|
||||||
|
@ -297,143 +290,3 @@ func stringsEqual(a, b []string) bool {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func tagsEqual(prev, current map[string]string) bool {
|
|
||||||
return reflect.DeepEqual(prev, current)
|
|
||||||
}
|
|
||||||
|
|
||||||
func columnsEqual(prev, current []string) bool {
|
|
||||||
return reflect.DeepEqual(prev, current)
|
|
||||||
}
|
|
||||||
|
|
||||||
func headersEqual(prev, current models.Row) bool {
|
|
||||||
if prev.Name != current.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return tagsEqual(prev.Tags, current.Tags) && columnsEqual(prev.Columns, current.Columns)
|
|
||||||
}
|
|
||||||
|
|
||||||
type textFormatter struct{}
|
|
||||||
|
|
||||||
func (f *textFormatter) WriteResponse(ctx context.Context, w io.Writer, resp Response) (err error) {
|
|
||||||
span, _ := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
if err := resp.Error(); err != nil {
|
|
||||||
fmt.Fprintln(w, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Create a tabbed writer for each result as they won't always line up
|
|
||||||
writer := new(tabwriter.Writer)
|
|
||||||
writer.Init(w, 0, 8, 1, ' ', 0)
|
|
||||||
|
|
||||||
var previousHeaders models.Row
|
|
||||||
for i, result := range resp.Results {
|
|
||||||
// Print out all messages first
|
|
||||||
for _, m := range result.Messages {
|
|
||||||
fmt.Fprintf(w, "%s: %s.\n", m.Level, m.Text)
|
|
||||||
}
|
|
||||||
// Check to see if the headers are the same as the previous row. If so, suppress them in the output
|
|
||||||
suppressHeaders := len(result.Series) > 0 && headersEqual(previousHeaders, *result.Series[0])
|
|
||||||
if !suppressHeaders && len(result.Series) > 0 {
|
|
||||||
previousHeaders = models.Row{
|
|
||||||
Name: result.Series[0].Name,
|
|
||||||
Tags: result.Series[0].Tags,
|
|
||||||
Columns: result.Series[0].Columns,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are suppressing headers, don't output the extra line return. If we
|
|
||||||
// aren't suppressing headers, then we put out line returns between results
|
|
||||||
// (not before the first result, and not after the last result).
|
|
||||||
if !suppressHeaders && i > 0 {
|
|
||||||
fmt.Fprintln(writer, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := f.formatResults(result.Series, "\t", suppressHeaders)
|
|
||||||
for _, r := range rows {
|
|
||||||
fmt.Fprintln(writer, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
_ = writer.Flush()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *textFormatter) formatResults(result models.Rows, separator string, suppressHeaders bool) []string {
|
|
||||||
var rows []string
|
|
||||||
// Create a tabbed writer for each result as they won't always line up
|
|
||||||
for i, row := range result {
|
|
||||||
// gather tags
|
|
||||||
var tags []string
|
|
||||||
for k, v := range row.Tags {
|
|
||||||
tags = append(tags, fmt.Sprintf("%s=%s", k, v))
|
|
||||||
sort.Strings(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
var columnNames []string
|
|
||||||
|
|
||||||
columnNames = append(columnNames, row.Columns...)
|
|
||||||
|
|
||||||
// Output a line separator if we have more than one set or results and format is column
|
|
||||||
if i > 0 && !suppressHeaders {
|
|
||||||
rows = append(rows, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are column format, we break out the name/tag to separate lines
|
|
||||||
if !suppressHeaders {
|
|
||||||
if row.Name != "" {
|
|
||||||
n := fmt.Sprintf("name: %s", row.Name)
|
|
||||||
rows = append(rows, n)
|
|
||||||
}
|
|
||||||
if len(tags) > 0 {
|
|
||||||
t := fmt.Sprintf("tags: %s", strings.Join(tags, ", "))
|
|
||||||
rows = append(rows, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !suppressHeaders {
|
|
||||||
rows = append(rows, strings.Join(columnNames, separator))
|
|
||||||
}
|
|
||||||
|
|
||||||
// if format is column, write dashes under each column
|
|
||||||
if !suppressHeaders {
|
|
||||||
var lines []string
|
|
||||||
for _, columnName := range columnNames {
|
|
||||||
lines = append(lines, strings.Repeat("-", len(columnName)))
|
|
||||||
}
|
|
||||||
rows = append(rows, strings.Join(lines, separator))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range row.Values {
|
|
||||||
var values []string
|
|
||||||
|
|
||||||
for _, vv := range v {
|
|
||||||
values = append(values, interfaceToString(vv))
|
|
||||||
}
|
|
||||||
rows = append(rows, strings.Join(values, separator))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
func interfaceToString(v interface{}) string {
|
|
||||||
switch t := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return ""
|
|
||||||
case bool:
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr:
|
|
||||||
return fmt.Sprintf("%d", t)
|
|
||||||
case float32:
|
|
||||||
// Default for floats via `fmt.Sprintf("%v", t)` is to represent them in scientific notation.
|
|
||||||
// We want to represent them as they are, with the least digits as possible (prec: -1).
|
|
||||||
return strconv.FormatFloat(float64(t), 'f', -1, 32)
|
|
||||||
case float64:
|
|
||||||
// Default for floats via `fmt.Sprintf("%v", t)` is to represent them in scientific notation.
|
|
||||||
// We want to represent them as they are, with the least digits as possible (prec: -1).
|
|
||||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ type EncodingFormat int
|
||||||
|
|
||||||
func (f *EncodingFormat) UnmarshalJSON(bytes []byte) error {
|
func (f *EncodingFormat) UnmarshalJSON(bytes []byte) error {
|
||||||
var s string
|
var s string
|
||||||
|
|
||||||
if err := json.Unmarshal(bytes, &s); err != nil {
|
if err := json.Unmarshal(bytes, &s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -24,23 +25,21 @@ func (f EncodingFormat) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EncodingFormatJSON EncodingFormat = iota
|
EncodingFormatJSON EncodingFormat = iota
|
||||||
EncodingFormatCSV
|
EncodingFormatTextCSV
|
||||||
|
EncodingFormatAppCSV
|
||||||
EncodingFormatMessagePack
|
EncodingFormatMessagePack
|
||||||
EncodingFormatTable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns closed encoding format from the specified mime type.
|
// Returns closed encoding format from the specified mime type.
|
||||||
// The default is JSON if no exact match is found.
|
// The default is JSON if no exact match is found.
|
||||||
func EncodingFormatFromMimeType(s string) EncodingFormat {
|
func EncodingFormatFromMimeType(s string) EncodingFormat {
|
||||||
switch s {
|
switch s {
|
||||||
case "application/csv", "text/csv":
|
case "application/csv":
|
||||||
return EncodingFormatCSV
|
return EncodingFormatAppCSV
|
||||||
case "text/plain":
|
case "text/csv":
|
||||||
return EncodingFormatTable
|
return EncodingFormatTextCSV
|
||||||
case "application/x-msgpack":
|
case "application/x-msgpack":
|
||||||
return EncodingFormatMessagePack
|
return EncodingFormatMessagePack
|
||||||
case "application/json":
|
|
||||||
fallthrough
|
|
||||||
default:
|
default:
|
||||||
return EncodingFormatJSON
|
return EncodingFormatJSON
|
||||||
}
|
}
|
||||||
|
@ -48,14 +47,12 @@ func EncodingFormatFromMimeType(s string) EncodingFormat {
|
||||||
|
|
||||||
func (f EncodingFormat) ContentType() string {
|
func (f EncodingFormat) ContentType() string {
|
||||||
switch f {
|
switch f {
|
||||||
case EncodingFormatCSV:
|
case EncodingFormatAppCSV:
|
||||||
|
return "application/csv"
|
||||||
|
case EncodingFormatTextCSV:
|
||||||
return "text/csv"
|
return "text/csv"
|
||||||
case EncodingFormatTable:
|
|
||||||
return "text/plain"
|
|
||||||
case EncodingFormatMessagePack:
|
case EncodingFormatMessagePack:
|
||||||
return "application/x-msgpack"
|
return "application/x-msgpack"
|
||||||
case EncodingFormatJSON:
|
|
||||||
fallthrough
|
|
||||||
default:
|
default:
|
||||||
return "application/json"
|
return "application/json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package influxql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodingFormatFromMimeType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s string
|
||||||
|
exp EncodingFormat
|
||||||
|
}{
|
||||||
|
{s: "application/csv", exp: EncodingFormatAppCSV},
|
||||||
|
{s: "text/csv", exp: EncodingFormatTextCSV},
|
||||||
|
{s: "application/x-msgpack", exp: EncodingFormatMessagePack},
|
||||||
|
{s: "application/json", exp: EncodingFormatJSON},
|
||||||
|
{s: "*/*", exp: EncodingFormatJSON},
|
||||||
|
{s: "", exp: EncodingFormatJSON},
|
||||||
|
{s: "application/other", exp: EncodingFormatJSON},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
|
got := EncodingFormatFromMimeType(tt.s)
|
||||||
|
assert.Equal(t, tt.exp, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue