410 lines
11 KiB
Go
410 lines
11 KiB
Go
// Copyright 2020 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package engine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/types"
|
|
"github.com/dolthub/vitess/go/sqltypes"
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/dolthub/dolt/go/cmd/dolt/cli"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/row"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/typed/json"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/typed/parquet"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/untyped/csv"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/untyped/tabular"
|
|
"github.com/dolthub/dolt/go/libraries/utils/iohelp"
|
|
"github.com/dolthub/dolt/go/store/util/outputpager"
|
|
)
|
|
|
|
type PrintResultFormat byte
|
|
|
|
const (
|
|
FormatTabular PrintResultFormat = iota
|
|
FormatCsv
|
|
FormatJson
|
|
FormatNull // used for profiling
|
|
FormatVertical
|
|
FormatParquet
|
|
)
|
|
|
|
type PrintSummaryBehavior byte
|
|
|
|
const (
|
|
PrintNoSummary PrintSummaryBehavior = 0
|
|
PrintRowCountAndTiming = 1
|
|
)
|
|
|
|
// PrettyPrintResults prints the result of a query in the format provided
|
|
func PrettyPrintResults(ctx *sql.Context, resultFormat PrintResultFormat, sqlSch sql.Schema, rowIter sql.RowIter, pageResults, showWarnings, printOkResult, binaryAsHex bool) (rerr error) {
|
|
return prettyPrintResultsWithSummary(ctx, resultFormat, sqlSch, rowIter, PrintNoSummary, pageResults, showWarnings, printOkResult, binaryAsHex)
|
|
}
|
|
|
|
// PrettyPrintResultsExtended prints the result of a query in the format provided, including row count and timing info
|
|
func PrettyPrintResultsExtended(ctx *sql.Context, resultFormat PrintResultFormat, sqlSch sql.Schema, rowIter sql.RowIter, pageResults, showWarnings, printOkResult, binaryAsHex bool) (rerr error) {
|
|
return prettyPrintResultsWithSummary(ctx, resultFormat, sqlSch, rowIter, PrintRowCountAndTiming, pageResults, showWarnings, printOkResult, binaryAsHex)
|
|
}
|
|
|
|
func prettyPrintResultsWithSummary(ctx *sql.Context, resultFormat PrintResultFormat, sqlSch sql.Schema, rowIter sql.RowIter, summary PrintSummaryBehavior, pageResults, showWarnings, printOkResult, binaryAsHex bool) (rerr error) {
|
|
defer func() {
|
|
closeErr := rowIter.Close(ctx)
|
|
if rerr == nil && closeErr != nil {
|
|
rerr = closeErr
|
|
}
|
|
}()
|
|
|
|
start := ctx.QueryTime()
|
|
|
|
// TODO: this isn't appropriate for JSON, CSV, other structured result formats
|
|
if isOkResult(sqlSch) {
|
|
// OkResult is only printed when we are in interactive terminal (TTY)
|
|
if !printOkResult {
|
|
return nil
|
|
}
|
|
return printOKResult(ctx, rowIter, start)
|
|
}
|
|
|
|
var wr table.SqlRowWriter
|
|
var err error
|
|
var numRows int
|
|
|
|
// Function to print results. A function is required because we need to wrap the whole process in a swap of
|
|
// IO streams. This is done with cli.ExecuteWithStdioRestored, which requires a resultless function. As
|
|
// a result, we need to depend on side effects to numRows and err to determine if it was successful.
|
|
printEm := func() {
|
|
writerStream := cli.CliOut
|
|
if pageResults {
|
|
pager := outputpager.Start()
|
|
defer pager.Stop()
|
|
writerStream = pager.Writer
|
|
}
|
|
|
|
switch resultFormat {
|
|
case FormatCsv:
|
|
var err error
|
|
wr, err = csv.NewCSVSqlWriter(iohelp.NopWrCloser(writerStream), sqlSch, csv.NewCSVInfo())
|
|
if err != nil {
|
|
return
|
|
}
|
|
case FormatJson:
|
|
var err error
|
|
wr, err = json.NewJSONSqlWriter(iohelp.NopWrCloser(writerStream), sqlSch)
|
|
if err != nil {
|
|
return
|
|
}
|
|
case FormatTabular:
|
|
wr = tabular.NewFixedWidthTableWriter(sqlSch, iohelp.NopWrCloser(writerStream), 100)
|
|
case FormatNull:
|
|
wr = nullWriter{}
|
|
case FormatVertical:
|
|
wr = newVerticalRowWriter(iohelp.NopWrCloser(writerStream), sqlSch)
|
|
case FormatParquet:
|
|
var err error
|
|
wr, err = parquet.NewParquetRowWriter(sqlSch, iohelp.NopWrCloser(writerStream))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Wrap iterator with binary-to-hex transformation if needed
|
|
if binaryAsHex {
|
|
rowIter = newBinaryHexIterator(rowIter, sqlSch)
|
|
}
|
|
|
|
numRows, err = writeResultSet(ctx, rowIter, wr)
|
|
}
|
|
|
|
if pageResults {
|
|
cli.ExecuteWithStdioRestored(printEm)
|
|
} else {
|
|
printEm()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if there is no row data and result format is JSON, then create empty JSON.
|
|
if resultFormat == FormatJson && numRows == 0 {
|
|
iohelp.WriteLine(cli.CliOut, "{}")
|
|
}
|
|
|
|
if summary == PrintRowCountAndTiming {
|
|
warnings := ""
|
|
if showWarnings {
|
|
warnings = "\n"
|
|
for _, warn := range ctx.Session.Warnings() {
|
|
warnings += color.YellowString(fmt.Sprintf("\nWarning (Code %d): %s", warn.Code, warn.Message))
|
|
}
|
|
}
|
|
|
|
err = printResultSetSummary(numRows, ctx.WarningCount(), warnings, start)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Some output formats need a final newline printed, others do not
|
|
switch resultFormat {
|
|
case FormatJson, FormatTabular, FormatVertical:
|
|
return iohelp.WriteLine(cli.CliOut, "")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func printResultSetSummary(numRows int, numWarnings uint16, warningsList string, start time.Time) error {
|
|
|
|
warning := ""
|
|
if numWarnings > 0 {
|
|
plural := ""
|
|
if numWarnings > 1 {
|
|
plural = "s"
|
|
}
|
|
warning = fmt.Sprintf(", %d warning%s", numWarnings, plural)
|
|
}
|
|
|
|
if numRows == 0 {
|
|
printEmptySetResult(start, warning)
|
|
return nil
|
|
}
|
|
|
|
noun := "rows"
|
|
if numRows == 1 {
|
|
noun = "row"
|
|
}
|
|
|
|
secondsSinceStart := secondsSince(start, time.Now())
|
|
err := iohelp.WriteLine(cli.CliOut, fmt.Sprintf("%d %s in set%s (%.2f sec) %s", numRows, noun, warning, secondsSinceStart, warningsList))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// binaryHexIterator wraps a row iterator and transforms binary data to hex format
|
|
type binaryHexIterator struct {
|
|
inner sql.RowIter
|
|
schema sql.Schema
|
|
}
|
|
|
|
var _ sql.RowIter = (*binaryHexIterator)(nil)
|
|
|
|
// newBinaryHexIterator creates a new iterator that transforms binary data to hex format
|
|
func newBinaryHexIterator(inner sql.RowIter, schema sql.Schema) sql.RowIter {
|
|
return &binaryHexIterator{
|
|
inner: inner,
|
|
schema: schema,
|
|
}
|
|
}
|
|
|
|
// Next returns the next row with binary data transformed to hex format
|
|
func (iter *binaryHexIterator) Next(ctx *sql.Context) (sql.Row, error) {
|
|
rowData, err := iter.inner.Next(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: Add support for BLOB types (TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB) and BIT type
|
|
for i, val := range rowData {
|
|
if val != nil && i < len(iter.schema) {
|
|
switch iter.schema[i].Type.Type() {
|
|
case sqltypes.Binary, sqltypes.VarBinary:
|
|
switch v := val.(type) {
|
|
case []byte: // hex fmt is explicitly upper case
|
|
rowData[i] = sqlutil.BinaryAsHexDisplayValue(fmt.Sprintf("0x%X", v))
|
|
case string: // handles results from sql-server; MySQL wire protocol returns strings
|
|
rowData[i] = sqlutil.BinaryAsHexDisplayValue(fmt.Sprintf("0x%X", []byte(v)))
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type %T for binary column %s", val, iter.schema[i].Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rowData, nil
|
|
}
|
|
|
|
// Close closes the wrapped iterator and releases any resources.
|
|
func (iter *binaryHexIterator) Close(ctx *sql.Context) error {
|
|
return iter.inner.Close(ctx)
|
|
}
|
|
|
|
// writeResultSet drains the iterator given, printing rows from it to the writer given. Returns the number of rows.
|
|
func writeResultSet(ctx *sql.Context, rowIter sql.RowIter, wr table.SqlRowWriter) (int, error) {
|
|
i := 0
|
|
for {
|
|
r, err := rowIter.Next(ctx)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
err = wr.WriteSqlRow(ctx, r)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
err := wr.Close(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
// secondsSince returns the number of full and partial seconds since the time given
|
|
func secondsSince(start time.Time, end time.Time) float64 {
|
|
runTime := end.Sub(start)
|
|
seconds := runTime / time.Second
|
|
milliRemainder := (runTime - seconds*time.Second) / time.Millisecond
|
|
timeDisplay := float64(seconds) + float64(milliRemainder)*.001
|
|
return timeDisplay
|
|
}
|
|
|
|
// nullWriter is a no-op SqlRowWriter implementation
|
|
type nullWriter struct{}
|
|
|
|
func (n nullWriter) WriteSqlRow(ctx *sql.Context, r sql.Row) error { return nil }
|
|
func (n nullWriter) Close(ctx context.Context) error { return nil }
|
|
|
|
func printEmptySetResult(start time.Time, warning string) {
|
|
seconds := secondsSince(start, time.Now())
|
|
cli.Printf("Empty set%s (%.2f sec)\n", warning, seconds)
|
|
}
|
|
|
|
func printOKResult(ctx *sql.Context, iter sql.RowIter, start time.Time) error {
|
|
row, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if okResult, ok := row[0].(types.OkResult); ok {
|
|
rowNoun := "row"
|
|
if okResult.RowsAffected != 1 {
|
|
rowNoun = "rows"
|
|
}
|
|
|
|
seconds := secondsSince(start, time.Now())
|
|
cli.Printf("Query OK, %d %s affected (%.2f sec)\n", okResult.RowsAffected, rowNoun, seconds)
|
|
|
|
if okResult.Info != nil {
|
|
cli.Printf("%s\n", okResult.Info)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isOkResult(sch sql.Schema) bool {
|
|
return sch.Equals(types.OkResultSchema)
|
|
}
|
|
|
|
type verticalRowWriter struct {
|
|
wr io.WriteCloser
|
|
sch sql.Schema
|
|
idx int
|
|
offsets []int
|
|
}
|
|
|
|
func newVerticalRowWriter(wr io.WriteCloser, sch sql.Schema) *verticalRowWriter {
|
|
return &verticalRowWriter{
|
|
wr: wr,
|
|
sch: sch,
|
|
offsets: calculateVerticalOffsets(sch),
|
|
}
|
|
}
|
|
|
|
func calculateVerticalOffsets(sch sql.Schema) []int {
|
|
offsets := make([]int, len(sch))
|
|
|
|
maxLen := 0
|
|
for i := range sch {
|
|
if len(sch[i].Name) > maxLen {
|
|
maxLen = len(sch[i].Name)
|
|
}
|
|
}
|
|
|
|
for i := range sch {
|
|
offsets[i] = maxLen - len(sch[i].Name)
|
|
}
|
|
|
|
return offsets
|
|
}
|
|
|
|
func (v *verticalRowWriter) WriteRow(ctx context.Context, r row.Row) error {
|
|
return errors.New("unimplemented")
|
|
}
|
|
|
|
func (v *verticalRowWriter) Close(ctx context.Context) error {
|
|
return v.wr.Close()
|
|
}
|
|
|
|
var space = []byte{' '}
|
|
|
|
func (v *verticalRowWriter) WriteSqlRow(ctx *sql.Context, r sql.Row) error {
|
|
v.idx++
|
|
sep := fmt.Sprintf("*************************** %d. row ***************************\n", v.idx)
|
|
_, err := v.wr.Write([]byte(sep))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range r {
|
|
for numSpaces := 0; numSpaces < v.offsets[i]; numSpaces++ {
|
|
_, err = v.wr.Write(space)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var str string
|
|
|
|
if r[i] == nil {
|
|
str = "NULL"
|
|
} else {
|
|
str, err = sqlutil.SqlColToStr(ctx, v.sch[i].Type, r[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = v.wr.Write([]byte(fmt.Sprintf("%s: %v\n", v.sch[i].Name, str)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = v.wr.Write([]byte("\n"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|