@@ -2,10 +2,10 @@ package query
22
33import (
44 "context"
5- "encoding/json"
65 "errors"
76 "fmt"
87 "io"
8+ "slices"
99 "time"
1010
1111 "github.com/AlecAivazis/survey/v2"
@@ -160,7 +160,7 @@ func run(ctx context.Context, opts *options) error {
160160 )
161161 if err != nil {
162162 return err
163- } else if len (res .Matches ) == 0 && len (res .Buckets . Totals ) == 0 {
163+ } else if res . Status . RowsMatched == 0 || len (res .Tables ) == 0 || len (res .Tables [ 0 ]. Columns ) == 0 {
164164 return errors .New ("query returned no results" )
165165 }
166166
@@ -172,88 +172,144 @@ func run(ctx context.Context, opts *options) error {
172172 }
173173 defer pagerStop ()
174174
175- // Deal with JSON output format.
176- if opts .Format == iofmt .JSON .String () {
177- var data any
178- if len (res .Matches ) > 0 {
179- data = res .Matches
180- } else if len (res .Buckets .Totals ) > 0 {
181- // For ungrouped buckets we just return the aggregated total.
182- if len (res .Buckets .Totals [0 ].Group ) == 0 {
183- data = res .Buckets .Totals [0 ].Aggregations [0 ].Value
184- } else {
185- data = res .Buckets .Totals
186- }
187- }
188- return iofmt .FormatToJSON (opts .IO .Out (), data , opts .IO .ColorEnabled ())
189- }
190-
191175 cs := opts .IO .ColorScheme ()
192176
193177 headerText := cs .Bold (opts .Query )
194178 headerText += fmt .Sprintf (" processed in %s" , cs .Gray (res .Status .ElapsedTime .String ()))
195179 headerText = fmt .Sprintf ("Result of query %s:\n \n " , headerText )
196180
197- // Deal with table output format for matches.
198- if len (res .Matches ) > 0 {
181+ table := res .Tables [0 ]
182+
183+ // Deal with JSON output format. It only works for non-aggregated results OR
184+ // an aggregated result which produces a single value (it has no groups).
185+ if opts .Format == iofmt .JSON .String () {
186+ if tableHasAggregation (table ) {
187+ if len (table .Groups ) > 1 || (len (table .Columns ) > 1 && len (table .Columns [0 ]) > 1 ) {
188+ return errors .New ("JSON output format is not supported for aggregated results with groups" )
189+ }
190+ if opts .IO .IsStdoutTTY () {
191+ fmt .Fprint (opts .IO .Out (), headerText )
192+ }
193+ return iofmt .FormatToJSON (opts .IO .Out (), table .Columns [0 ][0 ], opts .IO .ColorEnabled ())
194+ }
195+
199196 if opts .IO .IsStdoutTTY () {
200197 fmt .Fprint (opts .IO .Out (), headerText )
201198 }
202199
203- for _ , entry := range res .Matches {
204- var data any
205- switch opts .Format {
206- case iofmt .JSON .String ():
207- data = entry
208- default :
209- fmt .Fprintf (opts .IO .Out (), "%s\t " , cs .Gray (entry .Time .Format (time .RFC1123 )))
210- data = entry .Data
211- }
212- if err = iofmt .FormatToJSON (opts .IO .Out (), data , opts .IO .ColorEnabled ()); err != nil {
200+ for i := range len (table .Columns [0 ]) {
201+ if err = iofmt .FormatToJSON (opts .IO .Out (), tableRowAtIndex (table , i ), opts .IO .ColorEnabled ()); err != nil {
213202 return err
214203 }
215204 }
216205
217206 return nil
218207 }
219208
220- // Deal with table output format for grouped results.
221-
222- // If we have no groups, we just print the aggregated total.
223- if len (res .Buckets .Totals [0 ].Group ) == 0 {
209+ // Deal with table output for a result with more than ten fields as it
210+ // wouldn't be readable in a table.
211+ if ! tableHasAggregation (table ) && len (table .Fields ) > 10 {
224212 if opts .IO .IsStdoutTTY () {
225213 fmt .Fprint (opts .IO .Out (), headerText )
226214 }
227215
228- return iofmt .FormatToJSON (opts .IO .Out (),
229- res .Buckets .Totals [0 ].Aggregations [0 ].Value , opts .IO .ColorEnabled ())
216+ hasTimeField := slices .ContainsFunc (table .Fields , func (field query.Field ) bool {
217+ return field .Name == "_time"
218+ })
219+ hasSysTimeField := slices .ContainsFunc (table .Fields , func (field query.Field ) bool {
220+ return field .Name == "_sysTime"
221+ })
222+
223+ for i := range len (table .Columns [0 ]) {
224+ row := tableRowAtIndex (table , i )
225+ if hasTimeField {
226+ ts , err := time .Parse (time .RFC3339Nano , fmt .Sprint (row ["_time" ]))
227+ if err != nil {
228+ return err
229+ }
230+ fmt .Fprintf (opts .IO .Out (), "%s\t " , cs .Gray (ts .Format (time .RFC1123 )))
231+ delete (row , "_time" )
232+ }
233+ if hasSysTimeField {
234+ delete (row , "_sysTime" )
235+ }
236+ if err = iofmt .FormatToJSON (opts .IO .Out (), row , opts .IO .ColorEnabled ()); err != nil {
237+ return err
238+ }
239+ }
240+
241+ return nil
242+ }
243+
244+ // Deal with table output format for non-aggregated results.
245+ if ! tableHasAggregation (table ) {
246+ var header iofmt.HeaderBuilderFunc
247+ if opts .IO .IsStdoutTTY () {
248+ header = func (_ io.Writer , trb iofmt.TableRowBuilder ) {
249+ fmt .Fprint (opts .IO .Out (), headerText )
250+ for _ , field := range table .Fields {
251+ trb .AddField (field .Name , cs .Bold )
252+ }
253+ }
254+ }
255+
256+ contentRow := func (trb iofmt.TableRowBuilder , k int ) {
257+ for _ , column := range table .Columns {
258+ trb .AddField (fmt .Sprint (column [k ]), nil )
259+ }
260+ }
261+
262+ return iofmt .FormatToTable (opts .IO , len (table .Columns [0 ]), header , nil , contentRow )
230263 }
231264
232- // If we have groups, we print a table with the groups as columns and the
233- // aggregated totals as rows.
234- var (
235- header iofmt.HeaderBuilderFunc
236- columnNames = res .GroupBy
237- )
265+ // Deal with table output format for aggregated results.
266+ var header iofmt.HeaderBuilderFunc
238267 if opts .IO .IsStdoutTTY () {
239268 header = func (_ io.Writer , trb iofmt.TableRowBuilder ) {
240269 fmt .Fprint (opts .IO .Out (), headerText )
241- for _ , name := range columnNames {
242- trb .AddField (name , cs .Bold )
270+ for _ , field := range table . Fields {
271+ trb .AddField (field . Name , cs .Bold )
243272 }
244- trb .AddField (res .Buckets .Totals [0 ].Aggregations [0 ].Alias , cs .Bold )
245273 }
246274 }
247275
248276 contentRow := func (trb iofmt.TableRowBuilder , k int ) {
249- total := res .Buckets .Totals [k ]
250- aggValue , _ := json .Marshal (total .Aggregations [0 ].Value )
277+ for _ , column := range table .Columns {
278+ trb .AddField (fmt .Sprint (column [k ]), nil )
279+ }
280+ }
281+
282+ var footer iofmt.HeaderBuilderFunc
283+ if opts .IO .IsStdoutTTY () && len (res .Tables ) > 1 && res .Tables [1 ].Name == "_totals" {
284+ totalsTable := res .Tables [1 ]
285+ footer = func (_ io.Writer , trb iofmt.TableRowBuilder ) {
286+ trb .AddField ("Total" , cs .Bold ) // Account for the _time field present in the former table.
287+ for i , field := range totalsTable .Fields {
288+ var total string
289+ if field .Aggregation != nil {
290+ total = fmt .Sprint (totalsTable .Columns [i ][0 ])
291+ }
292+ trb .AddField (total , nil )
293+ }
294+ }
295+ }
296+
297+ return iofmt .FormatToTable (opts .IO , len (table .Columns [0 ]), header , footer , contentRow )
298+ }
251299
252- for _ , name := range columnNames {
253- trb .AddField (fmt .Sprint (total .Group [name ]), nil )
300+ func tableHasAggregation (table query.Table ) bool {
301+ for _ , field := range table .Fields {
302+ if field .Aggregation != nil {
303+ return true
254304 }
255- trb .AddField (string (aggValue ), nil )
256305 }
306+ return false
307+ }
257308
258- return iofmt .FormatToTable (opts .IO , len (res .Buckets .Totals ), header , nil , contentRow )
309+ func tableRowAtIndex (table query.Table , rowIdx int ) map [string ]any {
310+ row := make (map [string ]any , len (table .Fields ))
311+ for i , field := range table .Fields {
312+ row [field .Name ] = table.Columns [i ][rowIdx ]
313+ }
314+ return row
259315}
0 commit comments