Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

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
}