add \G vertical query format (#2593)

This commit is contained in:
jennifersp
2022-01-18 12:02:58 -08:00
committed by GitHub
parent 5ef1b4ab2b
commit ec95a96d9c
8 changed files with 208 additions and 6 deletions

View File

@@ -18,6 +18,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
@@ -39,6 +40,7 @@ const (
FormatCsv
FormatJson
FormatNull // used for profiling
FormatVertical
)
const (
@@ -71,6 +73,8 @@ func PrettyPrintResults(ctx *sql.Context, resultFormat PrintResultFormat, sqlSch
p = createTabularPipeline(ctx, sqlSch, rowIter)
case FormatNull:
p = createNullPipeline(ctx, sqlSch, rowIter)
case FormatVertical:
p = createVerticalPipeline(ctx, sqlSch, rowIter)
}
p.Start(ctx)
@@ -539,3 +543,117 @@ func genRowSepString(fwf fwt.FixedWidthFormatter) string {
return string(rowSepRunes)
}
// vertical format pipeline creation and pipeline functions
func createVerticalPipeline(ctx *sql.Context, sch sql.Schema, iter sql.RowIter) *pipeline.Pipeline {
const samplesForAutoSizing = 10000
vps := &verticalPipelineStages{}
p := pipeline.NewPipeline(
pipeline.NewStage("read", nil, getReadStageFunc(ctx, iter, readBatchSize), 0, 0, 0),
pipeline.NewStage("stringify", nil, rowsToStringSlices, 0, 1000, 1000),
pipeline.NewStage("fix_width", noParallelizationInitFunc, vps.getFixWidthStageFunc(samplesForAutoSizing), 0, 1000, readBatchSize),
pipeline.NewStage("cell_borders", noParallelizationInitFunc, vps.getSeparatorFunc(), 0, 1000, readBatchSize),
pipeline.NewStage("write", noParallelizationInitFunc, writeToCliOutStageFunc, 0, 100, writeBatchSize),
)
writeIn, _ := p.GetInputChannel("fix_width")
headers := make([]string, len(sch))
for i, col := range sch {
headers[i] = col.Name
}
writeIn <- []pipeline.ItemWithProps{
pipeline.NewItemWithProps(headers, pipeline.NewImmutableProps(map[string]interface{}{"headers": true})),
}
return p
}
type verticalPipelineStages struct {
heads []string
}
func (vps *verticalPipelineStages) getFixWidthStageFunc(samples int) func(context.Context, []pipeline.ItemWithProps) ([]pipeline.ItemWithProps, error) {
bufferring := true
buffer := make([]pipeline.ItemWithProps, 0, samples)
var maxWidth int
return func(_ context.Context, items []pipeline.ItemWithProps) ([]pipeline.ItemWithProps, error) {
if items == nil {
bufferring = false
return buffer, nil
}
if bufferring {
for _, item := range items {
props := item.GetProperties()
if _, ok := props.Get("headers"); ok {
head := item.GetItem().([]string)
for _, headStr := range head {
strWidth := fwt.StringWidth(headStr)
if strWidth > maxWidth {
maxWidth = strWidth
}
}
vps.heads, _ = vps.formatHeads(maxWidth, head)
} else {
buffer = append(buffer, item)
}
}
if len(buffer) > samples {
bufferring = false
ret := buffer
// clear the buffer
buffer = buffer[:0]
return ret, nil
}
return nil, nil
}
return items, nil
}
}
func (vps *verticalPipelineStages) formatHeads(width int, items []string) ([]string, error) {
results := make([]string, len(items))
for i, item := range items {
diff := width - len(item)
if diff < 0 {
return nil, errors.New("column width exceeded maximum width for column")
}
results[i] = fmt.Sprintf("%*s", width, item)
}
return results, nil
}
func (vps *verticalPipelineStages) getSeparatorFunc() func(context.Context, []pipeline.ItemWithProps) ([]pipeline.ItemWithProps, error) {
return func(_ context.Context, items []pipeline.ItemWithProps) ([]pipeline.ItemWithProps, error) {
empty := ""
if items == nil {
return []pipeline.ItemWithProps{pipeline.NewItemWithNoProps(&empty)}, nil
}
sb := &strings.Builder{}
sb.Grow(2048)
var sep string
idx := 0
for _, item := range items {
idx += 1
sep = fmt.Sprintf("*************************** %d. row ***************************\n", idx)
sb.WriteString(sep)
cols := item.GetItem().([]string)
for i, str := range cols {
sb.WriteString(vps.heads[i])
sb.WriteString(": ")
sb.WriteString(str)
sb.WriteString("\n")
}
}
str := sb.String()
return []pipeline.ItemWithProps{pipeline.NewItemWithNoProps(&str)}, nil
}
}

View File

@@ -131,7 +131,7 @@ func (cmd SqlCmd) CreateMarkdown(wr io.Writer, commandStr string) error {
func (cmd SqlCmd) ArgParser() *argparser.ArgParser {
ap := argparser.NewArgParser()
ap.SupportsString(QueryFlag, "q", "SQL query to run", "Runs a single query and exits")
ap.SupportsString(FormatFlag, "r", "result output format", "How to format result output. Valid values are tabular, csv, json. Defaults to tabular. ")
ap.SupportsString(FormatFlag, "r", "result output format", "How to format result output. Valid values are tabular, csv, json, vertical. Defaults to tabular. ")
ap.SupportsString(saveFlag, "s", "saved query name", "Used with --query, save the query to the query catalog with the name provided. Saved queries can be examined in the dolt_query_catalog system table.")
ap.SupportsString(executeFlag, "x", "saved query name", "Executes a saved query with the given name")
ap.SupportsFlag(listSavedFlag, "l", "Lists all saved queries")
@@ -539,6 +539,8 @@ func GetResultFormat(format string) (engine.PrintResultFormat, errhand.VerboseEr
return engine.FormatJson, nil
case "null":
return engine.FormatNull, nil
case "vertical":
return engine.FormatVertical, nil
default:
return engine.FormatTabular, errhand.BuildDError("Invalid argument for --result-format. Valid values are tabular, csv, json").Build()
}
@@ -744,12 +746,14 @@ func runShell(ctx context.Context, se *engine.SqlEngine, mrEnv *env.MultiRepoEnv
HistorySearchFold: true,
DisableAutoSaveHistory: true,
}
supportingMysqlShellCmds := []string{"\\g", "\\G"}
shellConf := ishell.UninterpretedConfig{
ReadlineConfig: &rlConf,
QuitKeywords: []string{
"quit", "exit", "quit()", "exit()",
},
LineTerminator: ";",
MysqlShellCmds: supportingMysqlShellCmds,
}
shell := ishell.NewUninterpreted(&shellConf)
@@ -793,6 +797,15 @@ func runShell(ctx context.Context, se *engine.SqlEngine, mrEnv *env.MultiRepoEnv
shell.Println(color.RedString(err.Error()))
}
returnFormat := se.GetReturnFormat()
query = strings.TrimSuffix(query, shell.LineTerminator())
for _, sc := range supportingMysqlShellCmds {
if strings.HasSuffix(query, "\\G") {
returnFormat = engine.FormatVertical
}
query = strings.TrimSuffix(query, sc)
}
//TODO: Handle comments and enforce the current line terminator
if matches := delimiterRegex.FindStringSubmatch(query); len(matches) == 3 {
// If we don't match from anything, then we just pass to the SQL engine and let it complain.
@@ -823,7 +836,7 @@ func runShell(ctx context.Context, se *engine.SqlEngine, mrEnv *env.MultiRepoEnv
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
err = engine.PrettyPrintResults(sqlCtx, se.GetReturnFormat(), sqlSch, rowIter, HasTopLevelOrderByClause(query))
err = engine.PrettyPrintResults(sqlCtx, returnFormat, sqlSch, rowIter, HasTopLevelOrderByClause(query))
if err != nil {
shell.Println(color.RedString(err.Error()))
}

View File

@@ -172,6 +172,7 @@ func (cmd SqlClientCmd) Exec(ctx context.Context, commandStr string, args []stri
"quit", "exit", "quit()", "exit()",
},
LineTerminator: ";",
MysqlShellCmds: []string{},
}
shell := ishell.NewUninterpreted(&shellConf)

View File

@@ -20,7 +20,7 @@ require (
github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20201005193433-3ee972b1d078
github.com/dolthub/fslock v0.0.3
github.com/dolthub/go-mysql-server v0.11.1-0.20220118100543-95025d39b521
github.com/dolthub/ishell v0.0.0-20210205014355-16a4ce758446
github.com/dolthub/ishell v0.0.0-20220112232610-14e753f0f371
github.com/dolthub/mmap-go v1.0.4-0.20201107010347-f9f2a9588a66
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81
github.com/dolthub/vitess v0.0.0-20220103175203-a8c7f5080008

View File

@@ -177,8 +177,8 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U=
github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0=
github.com/dolthub/go-mysql-server v0.11.1-0.20220118100543-95025d39b521 h1:0pGGT/HwdCKoy9vLQ3EgnI6iyqSg9M03xOIMnH9+FDY=
github.com/dolthub/go-mysql-server v0.11.1-0.20220118100543-95025d39b521/go.mod h1:tyrWU1vUzj/ilniOAefGJquvOpHNSrFSUHVWJqlSIFc=
github.com/dolthub/ishell v0.0.0-20210205014355-16a4ce758446 h1:0ol5pj+QlKUKAtqs1LiPM3ZJKs+rHPgLSsMXmhTrCAM=
github.com/dolthub/ishell v0.0.0-20210205014355-16a4ce758446/go.mod h1:dhGBqcCEfK5kuFmeO5+WOx3hqc1k3M29c1oS/R7N4ms=
github.com/dolthub/ishell v0.0.0-20220112232610-14e753f0f371 h1:oyPHJlzumKta1vnOQqUnfdz+pk3EmnHS3Nd0cCT0I2g=
github.com/dolthub/ishell v0.0.0-20220112232610-14e753f0f371/go.mod h1:dhGBqcCEfK5kuFmeO5+WOx3hqc1k3M29c1oS/R7N4ms=
github.com/dolthub/jsonpath v0.0.0-20210609232853-d49537a30474 h1:xTrR+l5l+1Lfq0NvhiEsctylXinUMFhhsqaEcl414p8=
github.com/dolthub/jsonpath v0.0.0-20210609232853-d49537a30474/go.mod h1:kMz7uXOXq4qRriCEyZ/LUeTqraLJCjf0WVZcUi6TxUY=
github.com/dolthub/mmap-go v1.0.4-0.20201107010347-f9f2a9588a66 h1:WRPDbpJWEnPxPmiuOTndT+lUWUeGjx6eoNOK9O4tQQQ=

View File

@@ -47,7 +47,7 @@ var ErrRowCountMismatch = errors.New("number of columns passed to formatter does
// ErrColumnTooLong is returned when the width exceeds the maximum
var ErrColumnTooLong = errors.New("column width exceeded maximum width for column and TooLongBehavior is ErrorWhenTooLong")
// FixedWithFormatter is a utility class for taking a row and generating fixed with output
// FixedWidthFormatter is a utility class for taking a row and generating fixed with output
type FixedWidthFormatter struct {
colCount int
Widths []int

View File

@@ -0,0 +1,45 @@
#!/usr/bin/expect
set timeout 10
spawn dolt sql
expect {
"> " { send "show tables\\G\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"*Table: one_pk*" { send "DELIMITER $$\\g\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"> " { send "show tables$$\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"*| Table |*" { send "show tables\\g\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"*| Table |*" { send "DELIMITER ;\\g\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"> " { send "SELECT COUNT(*) FROM one_pk\\G\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"*COUNT(\\*): 4*" { send "SELECT JSON_OBJECT('id', 87, 'name', 'carrot')\\G\r"; }
timeout { exit 1; }
failed { exit 1; }
}
expect {
"*JSON_OBJECT('id', 87, 'name', 'carrot'): {"id":87,"name":"carrot"}*" { exit 0 ; }
timeout { exit 1; }
failed { exit 1; }
}
expect eof

View File

@@ -1694,3 +1694,28 @@ SQL
get_head_commit() {
dolt log -n 1 | grep -m 1 commit | cut -c 15-46
}
@test "sql: sql -q query vertical format check" {
run dolt sql -r vertical -q "show tables"
[ "$status" -eq 0 ]
[ "$output" = "*************************** 1. row ***************************
Table: has_datetimes
*************************** 2. row ***************************
Table: one_pk
*************************** 3. row ***************************
Table: two_pk" ]
run dolt sql -r vertical -q "SELECT pk AS primaryKey FROM one_pk WHERE pk < 2"
[ "$status" -eq 0 ]
[ "$output" = "*************************** 1. row ***************************
primaryKey: 0
*************************** 2. row ***************************
primaryKey: 1" ]
}
@test "sql: vertical query format in sql shell" {
skiponwindows "Need to install expect and make this script work on windows."
run expect $BATS_TEST_DIRNAME/sql-vertical-format.expect
[ "$status" -eq 0 ]
}