mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-27 10:09:13 -06:00
add \G vertical query format (#2593)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
45
integration-tests/bats/sql-vertical-format.expect
Normal file
45
integration-tests/bats/sql-vertical-format.expect
Normal 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
|
||||
@@ -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 ]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user