mv impl to print layer

This commit is contained in:
elianddb
2025-07-25 20:06:31 +00:00
parent 77b0e3a7b4
commit a3e6bbaf88
13 changed files with 287 additions and 116 deletions

View File

@@ -130,7 +130,7 @@ func (cmd BlameCmd) Exec(ctx context.Context, commandStr string, args []string,
return 1
}
err = engine.PrettyPrintResults(sqlCtx, engine.FormatTabular, schema, ri, false, false, true)
err = engine.PrettyPrintResults(sqlCtx, engine.FormatTabular, schema, ri, false, false, true, false)
if err != nil {
iohelp.WriteLine(cli.CliOut, err.Error())
return 1

View File

@@ -181,7 +181,7 @@ func printViolationsForTable(ctx *sql.Context, dbName, tblName string, tbl *dolt
limitItr := &sqlLimitIter{itr: sqlItr, limit: 50}
err = engine.PrettyPrintResults(ctx, engine.FormatTabular, sqlSch, limitItr, false, false, false)
err = engine.PrettyPrintResults(ctx, engine.FormatTabular, sqlSch, limitItr, false, false, false, false)
if err != nil {
return errhand.BuildDError("Error outputting rows").AddCause(err).Build()
}

View File

@@ -23,6 +23,7 @@ import (
"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"
@@ -56,16 +57,16 @@ const (
)
// 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 bool) (rerr error) {
return prettyPrintResultsWithSummary(ctx, resultFormat, sqlSch, rowIter, PrintNoSummary, pageResults, showWarnings, printOkResult)
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 bool) (rerr error) {
return prettyPrintResultsWithSummary(ctx, resultFormat, sqlSch, rowIter, PrintRowCountAndTiming, pageResults, showWarnings, printOkResult)
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 bool) (rerr error) {
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 {
@@ -126,6 +127,11 @@ func prettyPrintResultsWithSummary(ctx *sql.Context, resultFormat PrintResultFor
}
}
// Wrap iterator with binary-to-hex transformation if needed
if binaryAsHex {
rowIter = newBinaryHexIterator(rowIter, sqlSch)
}
numRows, err = writeResultSet(ctx, rowIter, wr)
}
@@ -197,6 +203,55 @@ func printResultSetSummary(numRows int, numWarnings uint16, warningsList string,
return nil
}
// binaryHexIterator wraps a row iterator and transforms binary data to hex format
type binaryHexIterator struct {
inner sql.RowIter
schema sql.Schema
}
// newBinaryHexIterator creates a new iterator that transforms binary data to hex format.
// It wraps the provided iterator and transforms BINARY and VARBINARY column values
// to hex string representation (e.g., "0x41424344").
func newBinaryHexIterator(inner sql.RowIter, schema sql.Schema) sql.RowIter {
return &binaryHexIterator{
inner: inner,
schema: schema,
}
}
// Next returns the next row from the wrapped iterator with binary data transformed to hex format.
// Binary and VarBinary column values are converted to uppercase hex strings prefixed with "0x".
func (iter *binaryHexIterator) Next(ctx *sql.Context) (sql.Row, error) {
row, err := iter.inner.Next(ctx)
if err != nil {
return nil, err
}
// Transform binary column values to hex string format in place
// Currently supports BINARY and VARBINARY types only.
// TODO: Add support for BLOB types (TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB) and BIT type
// as confirmed by testing MySQL 8.4+ binary-as-hex behavior
for i, val := range row {
if val != nil && i < len(iter.schema) {
switch iter.schema[i].Type.Type() {
case sqltypes.Binary, sqltypes.VarBinary:
if bytes, ok := val.([]byte); ok {
row[i] = fmt.Sprintf("0x%X", bytes)
} else {
row[i] = fmt.Sprintf("0x%X", []byte(fmt.Sprint(val)))
}
}
}
}
return row, 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

View File

@@ -140,7 +140,7 @@ func (cmd TagsCmd) Exec(ctx context.Context, commandStr string, args []string, d
}
sqlCtx := sql.NewContext(ctx)
err = engine.PrettyPrintResults(sqlCtx, outputFmt, headerSchema, sql.RowsToRowIter(rows...), false, false, false)
err = engine.PrettyPrintResults(sqlCtx, outputFmt, headerSchema, sql.RowsToRowIter(rows...), false, false, false, false)
return commands.HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

View File

@@ -112,52 +112,6 @@ func init() {
dsqle.AddDoltSystemVariables()
}
// binaryAsHexRowIter wraps a row iterator and converts binary data to hex format
type binaryAsHexRowIter struct {
inner sql.RowIter
schema sql.Schema
}
func newBinaryAsHexRowIter(inner sql.RowIter, schema sql.Schema) sql.RowIter {
return &binaryAsHexRowIter{
inner: inner,
schema: schema,
}
}
func (iter *binaryAsHexRowIter) Next(ctx *sql.Context) (sql.Row, error) {
row, err := iter.inner.Next(ctx)
if err != nil {
return nil, err
}
// Transform binary data to hex format
for i, val := range row {
if val != nil && i < len(iter.schema) {
if stringType, ok := iter.schema[i].Type.(sql.StringType); ok {
sqlType := stringType.Type()
switch sqlType {
case sqltypes.Binary, sqltypes.VarBinary:
// Handle byte slice for VARBINARY/BINARY columns (local connections)
if binaryBytes, ok := val.([]byte); ok {
row[i] = fmt.Sprintf("0x%X", binaryBytes)
} else if binaryString, ok := val.(string); ok {
// Handle string data that contains binary (server connections)
row[i] = fmt.Sprintf("0x%X", []byte(binaryString))
}
}
}
}
}
return row, nil
}
func (iter *binaryAsHexRowIter) Close(ctx *sql.Context) error {
return iter.inner.Close(ctx)
}
type SqlCmd struct {
VersionStr string
}
@@ -197,8 +151,8 @@ func (cmd SqlCmd) ArgParser() *argparser.ArgParser {
ap.SupportsFlag(continueFlag, "c", "Continue running queries on an error. Used for batch mode only.")
ap.SupportsString(fileInputFlag, "f", "input file", "Execute statements from the file given.")
ap.SupportsFlag(binaryAsHexFlag, "", "Print binary data as hex. Enabled by default for interactive terminals.")
// --skip-binary-as-hex is supported but not shown in help, matching MySQL's behavior
ap.SupportsFlag(skipBinaryAsHexFlag, "", "")
// TODO: MySQL uses a skip- pattern for negating flags and doesn't show them in help
ap.SupportsFlag(skipBinaryAsHexFlag, "", "Disable binary data as hex output.")
return ap
}
@@ -265,19 +219,8 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
// Determine if we're running in a TTY (used by all modes)
isTty := false
fi, err := os.Stdin.Stat()
if err != nil {
if !osutil.IsWindows {
return HandleVErrAndExitCode(errhand.BuildDError("Couldn't stat STDIN. This is a bug.").Build(), usage)
}
} else {
isTty = fi.Mode()&os.ModeCharDevice != 0
}
// Determine binary-as-hex behavior once (default based on TTY, flags can override)
binaryAsHex := isTty
// Determine binary-as-hex behavior from flags (default false for non-interactive modes)
binaryAsHex := false
if apr.Contains(skipBinaryAsHexFlag) {
binaryAsHex = false
} else if apr.Contains(binaryAsHexFlag) {
@@ -303,6 +246,15 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE
return listSavedQueries(sqlCtx, queryist, dEnv, format, usage)
} else {
// Run in either batch mode for piped input, or shell mode for interactive
isTty := false
fi, err := os.Stdin.Stat()
if err != nil {
if !osutil.IsWindows {
return sqlHandleVErrAndExitCode(queryist, errhand.BuildDError("Couldn't stat STDIN. This is a bug.").Build(), usage)
}
} else {
isTty = fi.Mode()&os.ModeCharDevice != 0
}
_, continueOnError := apr.GetValue(continueFlag)
@@ -326,7 +278,9 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE
}
if isTty {
err := execShell(sqlCtx, queryist, format, cliCtx, binaryAsHex)
// In shell mode, default to hex format unless explicitly disabled
shellBinaryAsHex := !apr.Contains(skipBinaryAsHexFlag)
err := execShell(sqlCtx, queryist, format, cliCtx, shellBinaryAsHex)
if err != nil {
return sqlHandleVErrAndExitCode(queryist, errhand.VerboseErrorFromError(err), usage)
}
@@ -513,10 +467,7 @@ func execSingleQuery(
if rowIter != nil {
// Apply binary-as-hex formatting if enabled
if binaryAsHex {
rowIter = newBinaryAsHexRowIter(rowIter, sqlSch)
}
err = engine.PrettyPrintResults(sqlCtx, format, sqlSch, rowIter, false, false, false)
err = engine.PrettyPrintResults(sqlCtx, format, sqlSch, rowIter, false, false, false, binaryAsHex)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
@@ -732,11 +683,7 @@ func execBatchMode(ctx *sql.Context, qryist cli.Queryist, input io.Reader, conti
fileReadProg.printNewLineIfNeeded()
}
}
// Apply binary-as-hex formatting if enabled
if binaryAsHex {
rowIter = newBinaryAsHexRowIter(rowIter, sqlSch)
}
err = engine.PrettyPrintResults(ctx, format, sqlSch, rowIter, false, false, false)
err = engine.PrettyPrintResults(ctx, format, sqlSch, rowIter, false, false, false, binaryAsHex)
if err != nil {
err = buildBatchSqlErr(scanner.state.statementStartLine, query, err)
if !continueOnErr {
@@ -917,15 +864,11 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
// Apply binary-as-hex formatting to row data if enabled
if binaryAsHex {
rowIter = newBinaryAsHexRowIter(rowIter, sqlSch)
}
switch closureFormat {
case engine.FormatTabular, engine.FormatVertical:
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter, pagerEnabled, toggleWarnings, true)
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter, pagerEnabled, toggleWarnings, true, binaryAsHex)
default:
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter, pagerEnabled, toggleWarnings, true)
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter, pagerEnabled, toggleWarnings, true, binaryAsHex)
}
} else {
if _, isUseStmt := sqlStmt.(*sqlparser.Use); isUseStmt {

View File

@@ -23,8 +23,6 @@ import (
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/vitess/go/sqltypes"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/go-sql-driver/mysql"
"github.com/gocraft/dbr/v2"
@@ -33,11 +31,11 @@ import (
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/libraries/doltcore/servercfg"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
)
// BuildConnectionStringQueryist returns a Queryist that connects to the server specified by the given server config. Presence in this
// module isn't ideal, but it's the only way to get the server config into the queryist.
func BuildConnectionStringQueryist(ctx context.Context, cwdFS filesys.Filesys, creds *cli.UserPassword, apr *argparser.ArgParseResults, host string, port int, useTLS bool, dbRev string) (cli.LateBindQueryist, error) {
@@ -158,20 +156,9 @@ func NewMysqlRowWrapper(sqlRows *sql2.Rows) (*MysqlRowWrapper, error) {
iRow := make([]interface{}, len(colTypes))
rows := make([]sql.Row, 0)
for i, colType := range colTypes {
// Check if this is a binary type by examining the database type name
typeName := strings.ToLower(colType.DatabaseTypeName())
var sqlType sql.Type
switch typeName {
case "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob":
sqlType = types.MustCreateBinary(sqltypes.VarBinary, 255)
default:
// Default to LongText for all other types (as was done before)
sqlType = types.LongText
}
schema[i] = &sql.Column{
Name: colType.Name(),
Type: sqlType,
Type: sqlutil.DatabaseTypeNameToSqlType(colType.DatabaseTypeName()),
Nullable: true,
}
iRow[i] = &vRow[i]

View File

@@ -18,10 +18,14 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/dolthub/go-mysql-server/sql"
gmstypes "github.com/dolthub/go-mysql-server/sql/types"
// Necessary for the empty context used by some functions to be initialized with system vars
_ "github.com/dolthub/go-mysql-server/sql/variables"
"github.com/dolthub/vitess/go/sqltypes"
"github.com/dolthub/dolt/go/libraries/doltcore/row"
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
@@ -248,3 +252,21 @@ func SqlColToStr(ctx *sql.Context, sqlType sql.Type, col interface{}) (string, e
return "", nil
}
// DatabaseTypeNameToSqlType converts a MySQL wire protocol database type name
// to a go-mysql-server sql.Type. This uses the same type mapping logic as the existing
// Dolt type system for consistency.
// TODO: Add support for BLOB types (TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB) and BIT type
// as confirmed by testing MySQL 8.4+ binary-as-hex behavior
func DatabaseTypeNameToSqlType(databaseTypeName string) sql.Type {
typeName := strings.ToLower(databaseTypeName)
switch typeName {
case "binary":
return gmstypes.MustCreateBinary(sqltypes.Binary, 255)
case "varbinary":
return gmstypes.MustCreateBinary(sqltypes.VarBinary, 255)
default:
// Default to LongText for all other types (as was done before)
return gmstypes.LongText
}
}

View File

@@ -32,7 +32,6 @@ import (
const writeBufSize = 256 * 1024
// FixedWidthTableWriter is a TableWriter that applies a fixed width transform to its fields. All fields are
// expected to be strings.
type FixedWidthTableWriter struct {

View File

@@ -0,0 +1,46 @@
#!/usr/bin/expect
set port [lindex $argv 0]
set timeout 10
spawn dolt --host 127.0.0.1 --port $port --no-tls sql --binary-as-hex --skip-binary-as-hex
expect ">"
send "USE repo1;\r"
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 2;\r"
expect {
"0x616263" {
send "exit\r"
expect eof
exit 1
}
"abc" {
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 1;\r"
expect {
"0x0A000000" {
send "exit\r"
expect eof
exit 1
}
-re "1.*\n" {
expect ">"
send "exit\r"
expect eof
exit 0
}
timeout {
send "exit\r"
expect eof
exit 1
}
}
}
timeout {
send "exit\r"
expect eof
exit 1
}
}

View File

@@ -0,0 +1,41 @@
#!/usr/bin/expect
set port [lindex $argv 0]
set timeout 10
spawn dolt --host 127.0.0.1 --port $port --no-tls sql
expect ">"
send "USE repo1;\r"
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 1;\r"
expect {
"0x0A000000" {
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 2;\r"
expect {
"0x616263" {
expect ">"
send "exit\r"
expect eof
exit 0
}
"abc" {
send "exit\r"
expect eof
exit 1
}
timeout {
send "exit\r"
expect eof
exit 1
}
}
}
timeout {
send "exit\r"
expect eof
exit 1
}
}

View File

@@ -9,14 +9,29 @@ expect ">"
send "USE repo1;\r"
expect ">"
send "SELECT * FROM binary_test;\r"
send "SELECT pk, vb FROM binary_test WHERE pk = 2;\r"
expect {
"0x0A000000" {
# Found hex output - this is correct
"0x616263" {
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 1;\r"
expect {
"0x0A000000" {
expect ">"
send "exit\r"
expect eof
exit 0
}
timeout {
send "exit\r"
expect eof
exit 1
}
}
}
"abc" {
send "exit\r"
expect eof
exit 0
exit 1
}
timeout {
send "exit\r"

View File

@@ -9,14 +9,34 @@ expect ">"
send "USE repo1;\r"
expect ">"
send "SELECT * FROM binary_test;\r"
send "SELECT pk, vb FROM binary_test WHERE pk = 2;\r"
expect {
-re "\\| 1 \\|.*\\|" {
# Found raw binary output (not hex) - this is correct for skip flag
expect ">"
"0x616263" {
send "exit\r"
expect eof
exit 0
exit 1
}
"abc" {
expect ">"
send "SELECT pk, vb FROM binary_test WHERE pk = 1;\r"
expect {
"0x0A000000" {
send "exit\r"
expect eof
exit 1
}
-re "1.*\n" {
expect ">"
send "exit\r"
expect eof
exit 0
}
timeout {
send "exit\r"
expect eof
exit 1
}
}
}
timeout {
send "exit\r"

View File

@@ -2199,7 +2199,7 @@ EOF
}
@test "sql-server: client interactive binary-as-hex behavior works with server connections" {
@test "sql-server: client binary-as-hex behavior works with server connections" {
skiponwindows "Missing dependencies"
which expect > /dev/null || skip "expect not available"
@@ -2212,16 +2212,59 @@ EOF
start_sql_server repo1
# Test --binary-as-hex flag in interactive mode (should show hex)
run expect "$BATS_TEST_DIRNAME/binary-as-hex-server-interactive.expect" $PORT
echo "EXPECT OUTPUT: $output"
echo "EXPECT STATUS: $status"
# 1. Test default interactive behavior (should show hex by default)
run expect "$BATS_TEST_DIRNAME/binary-as-hex-server-default-interactive.expect" $PORT
[ $status -eq 0 ]
# Test --skip-binary-as-hex flag in interactive mode (should show raw)
# 2. Test --binary-as-hex flag in interactive mode (should show hex)
run expect "$BATS_TEST_DIRNAME/binary-as-hex-server-interactive.expect" $PORT
[ $status -eq 0 ]
# 3. Test --skip-binary-as-hex flag in interactive mode (should show raw)
run expect "$BATS_TEST_DIRNAME/binary-as-hex-skip-flag-interactive.expect" $PORT
[ $status -eq 0 ]
# 4. Test flag precedence: --skip-binary-as-hex overrides --binary-as-hex
run expect "$BATS_TEST_DIRNAME/binary-as-hex-flag-precedence-interactive.expect" $PORT
[ $status -eq 0 ]
# 5. Test non-interactive server behavior with -q flag (should show raw by default)
run dolt --host 127.0.0.1 --port $PORT --no-tls sql -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 1"
[ $status -eq 0 ]
! [[ "$output" =~ "0x0A000000" ]] || false
# 6. Test non-interactive server behavior with --binary-as-hex flag
run dolt --host 127.0.0.1 --port $PORT --no-tls sql --binary-as-hex -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 1"
[ $status -eq 0 ]
[[ "$output" =~ "0x0A000000" ]] || false
# 7. Test non-interactive server behavior with printable data
run dolt --host 127.0.0.1 --port $PORT --no-tls sql -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 2"
[ $status -eq 0 ]
[[ "$output" =~ "abc" ]] || false
# 8. Test non-interactive server flag precedence
run dolt --host 127.0.0.1 --port $PORT --no-tls sql --binary-as-hex --skip-binary-as-hex -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 2"
[ $status -eq 0 ]
[[ "$output" =~ "abc" ]] || false
! [[ "$output" =~ "0x616263" ]] || false
# 9. Test non-interactive server behavior with -q flag (should show raw by default)
run dolt --host 127.0.0.1 --port $PORT --no-tls sql -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 1"
[ $status -eq 0 ]
! [[ "$output" =~ "0x0A000000" ]] || false
# 10. Test non-interactive server behavior with -q and --binary-as-hex flags
run dolt --host 127.0.0.1 --port $PORT --no-tls sql --binary-as-hex -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 1"
[ $status -eq 0 ]
[[ "$output" =~ "0x0A000000" ]] || false
# 11. Test non-interactive server -q flag precedence
run dolt --host 127.0.0.1 --port $PORT --no-tls sql --binary-as-hex --skip-binary-as-hex -q "USE repo1; SELECT vb FROM binary_test WHERE pk = 2"
[ $status -eq 0 ]
[[ "$output" =~ "abc" ]] || false
! [[ "$output" =~ "0x616263" ]] || false
stop_sql_server 1
}