diff --git a/go/cmd/dolt/commands/blame.go b/go/cmd/dolt/commands/blame.go index 0ec7b9e5b9e..c22045b0ed0 100644 --- a/go/cmd/dolt/commands/blame.go +++ b/go/cmd/dolt/commands/blame.go @@ -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 diff --git a/go/cmd/dolt/commands/cvcmds/verify_constraints.go b/go/cmd/dolt/commands/cvcmds/verify_constraints.go index 3445cb69105..e473978781f 100644 --- a/go/cmd/dolt/commands/cvcmds/verify_constraints.go +++ b/go/cmd/dolt/commands/cvcmds/verify_constraints.go @@ -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() } diff --git a/go/cmd/dolt/commands/debug.go b/go/cmd/dolt/commands/debug.go index 059a41f1cde..490e492e5eb 100644 --- a/go/cmd/dolt/commands/debug.go +++ b/go/cmd/dolt/commands/debug.go @@ -436,5 +436,5 @@ func execDebugMode(ctx *sql.Context, qryist cli.Queryist, queryFile *os.File, co }() input := bufio.NewReader(transform.NewReader(queryFile, textunicode.BOMOverride(transform.Nop))) - return execBatchMode(ctx, qryist, input, continueOnErr, format) + return execBatchMode(ctx, qryist, input, continueOnErr, format, false) } diff --git a/go/cmd/dolt/commands/engine/sql_print.go b/go/cmd/dolt/commands/engine/sql_print.go index ba715a62b7b..4536792649c 100644 --- a/go/cmd/dolt/commands/engine/sql_print.go +++ b/go/cmd/dolt/commands/engine/sql_print.go @@ -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,54 @@ 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 +} + +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 diff --git a/go/cmd/dolt/commands/schcmds/tags.go b/go/cmd/dolt/commands/schcmds/tags.go index a790d1ae3c9..f5fcf23bd33 100644 --- a/go/cmd/dolt/commands/schcmds/tags.go +++ b/go/cmd/dolt/commands/schcmds/tags.go @@ -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) } diff --git a/go/cmd/dolt/commands/sql.go b/go/cmd/dolt/commands/sql.go index 3774b1183ef..d93774f4438 100644 --- a/go/cmd/dolt/commands/sql.go +++ b/go/cmd/dolt/commands/sql.go @@ -98,6 +98,11 @@ const ( ProfileFlag = "profile" timeFlag = "time" outputFlag = "output" + binaryAsHexFlag = "binary-as-hex" + skipBinaryAsHexFlag = "skip-binary-as-hex" + // TODO: Consider simplifying to use MySQL's skip pattern with single flag definition + // MySQL handles both --binary-as-hex and --skip-binary-as-hex with one option definition + // and uses disabled_my_option to distinguish between enable/disable welcomeMsg = `# Welcome to the DoltSQL shell. # Statements must be terminated with ';'. @@ -147,6 +152,9 @@ func (cmd SqlCmd) ArgParser() *argparser.ArgParser { ap.SupportsFlag(BatchFlag, "b", "Use to enable more efficient batch processing for large SQL import scripts. This mode is no longer supported and this flag is a no-op. To speed up your SQL imports, use either LOAD DATA, or structure your SQL import script to insert many rows per statement.") 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.") + // 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 } @@ -213,6 +221,12 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) } + // Determine binary-as-hex behavior from flags (default false for non-interactive modes) + binaryAsHex := apr.Contains(binaryAsHexFlag) + if binaryAsHex && apr.Contains(skipBinaryAsHexFlag) { // We stray from MYSQL here to make usage clear for users + return HandleVErrAndExitCode(errhand.BuildDError("cannot use both --%s and --%s", binaryAsHexFlag, skipBinaryAsHexFlag).Build(), usage) + } + queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx) if err != nil { return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) @@ -223,17 +237,16 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE if query, queryOK := apr.GetValue(QueryFlag); queryOK { if apr.Contains(saveFlag) { - return SaveQuery(sqlCtx, queryist, apr, query, format, usage) + return SaveQuery(sqlCtx, queryist, apr, query, format, usage, binaryAsHex) } - return queryMode(sqlCtx, queryist, apr, query, format, usage) + return queryMode(sqlCtx, queryist, apr, query, format, usage, binaryAsHex) } else if savedQueryName, exOk := apr.GetValue(executeFlag); exOk { - return executeSavedQuery(sqlCtx, queryist, savedQueryName, format, usage) + return executeSavedQuery(sqlCtx, queryist, savedQueryName, format, usage, binaryAsHex) } else if apr.Contains(listSavedFlag) { return listSavedQueries(sqlCtx, queryist, 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 { @@ -265,13 +278,15 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE } if isTty { - err := execShell(sqlCtx, queryist, format, cliCtx) + // 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) } } else { input = transform.NewReader(input, textunicode.BOMOverride(transform.Nop)) - err := execBatchMode(sqlCtx, queryist, input, continueOnError, format) + err := execBatchMode(sqlCtx, queryist, input, continueOnError, format, binaryAsHex) if err != nil { return sqlHandleVErrAndExitCode(queryist, errhand.VerboseErrorFromError(err), usage) } @@ -341,10 +356,10 @@ func (cmd SqlCmd) handleLegacyArguments(ap *argparser.ArgParser, commandStr stri func listSavedQueries(ctx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat, usage cli.UsagePrinter) int { query := "SELECT * FROM " + doltdb.DoltQueryCatalogTableName - return sqlHandleVErrAndExitCode(qryist, execSingleQuery(ctx, qryist, query, format), usage) + return sqlHandleVErrAndExitCode(qryist, execSingleQuery(ctx, qryist, query, format, false), usage) } -func executeSavedQuery(ctx *sql.Context, qryist cli.Queryist, savedQueryName string, format engine.PrintResultFormat, usage cli.UsagePrinter) int { +func executeSavedQuery(ctx *sql.Context, qryist cli.Queryist, savedQueryName string, format engine.PrintResultFormat, usage cli.UsagePrinter, binaryAsHex bool) int { var buffer bytes.Buffer buffer.WriteString("SELECT query FROM dolt_query_catalog where id = ?") searchQuery, err := dbr.InterpolateForDialect(buffer.String(), []interface{}{savedQueryName}, dialect.MySQL) @@ -370,7 +385,7 @@ func executeSavedQuery(ctx *sql.Context, qryist cli.Queryist, savedQueryName str } cli.PrintErrf("Executing saved query '%s':\n%s\n", savedQueryName, query) - return sqlHandleVErrAndExitCode(qryist, execSingleQuery(ctx, qryist, query, format), usage) + return sqlHandleVErrAndExitCode(qryist, execSingleQuery(ctx, qryist, query, format, binaryAsHex), usage) } func queryMode( @@ -380,12 +395,12 @@ func queryMode( query string, format engine.PrintResultFormat, usage cli.UsagePrinter, + binaryAsHex bool, ) int { - _, continueOnError := apr.GetValue(continueFlag) input := strings.NewReader(query) - err := execBatchMode(ctx, qryist, input, continueOnError, format) + err := execBatchMode(ctx, qryist, input, continueOnError, format, binaryAsHex) if err != nil { return sqlHandleVErrAndExitCode(qryist, errhand.VerboseErrorFromError(err), usage) } @@ -393,10 +408,10 @@ func queryMode( return 0 } -func SaveQuery(ctx *sql.Context, qryist cli.Queryist, apr *argparser.ArgParseResults, query string, format engine.PrintResultFormat, usage cli.UsagePrinter) int { +func SaveQuery(ctx *sql.Context, qryist cli.Queryist, apr *argparser.ArgParseResults, query string, format engine.PrintResultFormat, usage cli.UsagePrinter, binaryAsHex bool) int { saveName := apr.GetValueOrDefault(saveFlag, "") - verr := execSingleQuery(ctx, qryist, query, format) + verr := execSingleQuery(ctx, qryist, query, format, binaryAsHex) if verr != nil { return sqlHandleVErrAndExitCode(qryist, verr, usage) } @@ -435,6 +450,7 @@ func execSingleQuery( qryist cli.Queryist, query string, format engine.PrintResultFormat, + binaryAsHex bool, ) errhand.VerboseError { sqlSch, rowIter, _, err := processQuery(sqlCtx, query, qryist) @@ -443,7 +459,7 @@ func execSingleQuery( } if rowIter != nil { - 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) } @@ -601,7 +617,7 @@ func validateSqlArgs(apr *argparser.ArgParseResults) error { } // execBatchMode runs all the queries in the input reader -func execBatchMode(ctx *sql.Context, qryist cli.Queryist, input io.Reader, continueOnErr bool, format engine.PrintResultFormat) error { +func execBatchMode(ctx *sql.Context, qryist cli.Queryist, input io.Reader, continueOnErr bool, format engine.PrintResultFormat, binaryAsHex bool) error { scanner := NewStreamScanner(input) var query string for scanner.Scan() { @@ -649,7 +665,7 @@ func execBatchMode(ctx *sql.Context, qryist cli.Queryist, input io.Reader, conti fileReadProg.printNewLineIfNeeded() } } - 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 { @@ -675,7 +691,7 @@ func buildBatchSqlErr(stmtStartLine int, query string, err error) error { // execShell starts a SQL shell. Returns when the user exits the shell. The Root of the sqlEngine may // be updated by any queries which were processed. -func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat, cliCtx cli.CliContext) error { +func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat, cliCtx cli.CliContext, binaryAsHex bool) error { _ = iohelp.WriteLine(cli.CliOut, welcomeMsg) historyFile := filepath.Join(".sqlhistory") // history file written to working dir @@ -832,9 +848,9 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu } else if rowIter != nil { 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 { diff --git a/go/cmd/dolt/commands/sqlserver/queryist_utils.go b/go/cmd/dolt/commands/sqlserver/queryist_utils.go index 5af74de4260..ec04c6cde1d 100644 --- a/go/cmd/dolt/commands/sqlserver/queryist_utils.go +++ b/go/cmd/dolt/commands/sqlserver/queryist_utils.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/dolthub/go-mysql-server/sql" - "github.com/dolthub/go-mysql-server/sql/types" "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/go-sql-driver/mysql" "github.com/gocraft/dbr/v2" @@ -32,6 +31,7 @@ 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" ) @@ -147,18 +147,18 @@ type MysqlRowWrapper struct { var _ sql.RowIter = (*MysqlRowWrapper)(nil) func NewMysqlRowWrapper(sqlRows *sql2.Rows) (*MysqlRowWrapper, error) { - colNames, err := sqlRows.Columns() + colTypes, err := sqlRows.ColumnTypes() if err != nil { return nil, err } - schema := make(sql.Schema, len(colNames)) - vRow := make([]*string, len(colNames)) - iRow := make([]interface{}, len(colNames)) + schema := make(sql.Schema, len(colTypes)) + vRow := make([]*string, len(colTypes)) + iRow := make([]interface{}, len(colTypes)) rows := make([]sql.Row, 0) - for i, colName := range colNames { + for i, colType := range colTypes { schema[i] = &sql.Column{ - Name: colName, - Type: types.LongText, + Name: colType.Name(), + Type: sqlutil.DatabaseTypeNameToSqlType(colType.DatabaseTypeName()), Nullable: true, } iRow[i] = &vRow[i] diff --git a/go/libraries/doltcore/sqle/sqlutil/sql_row.go b/go/libraries/doltcore/sqle/sqlutil/sql_row.go index f8b3a99400d..44bee564c88 100644 --- a/go/libraries/doltcore/sqle/sqlutil/sql_row.go +++ b/go/libraries/doltcore/sqle/sqlutil/sql_row.go @@ -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" @@ -219,10 +223,17 @@ func keylessDoltRowFromSqlRow(ctx context.Context, vrw types.ValueReadWriter, sq return row.KeylessRow(vrw.Format(), vals[:j]...) } +// BinaryAsHexDisplayValue is a wrapper for binary values that should be displayed as hex strings. +type BinaryAsHexDisplayValue string + // SqlColToStr is a utility function for converting a sql column of type interface{} to a string. // NULL values are treated as empty strings. Handle nil separately if you require other behavior. func SqlColToStr(ctx *sql.Context, sqlType sql.Type, col interface{}) (string, error) { if col != nil { + if hexVal, ok := col.(BinaryAsHexDisplayValue); ok { + return string(hexVal), nil + } + switch typedCol := col.(type) { case bool: if typedCol { @@ -248,3 +259,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 + } +} diff --git a/integration-tests/bats/helper/common_expect_functions.tcl b/integration-tests/bats/helper/common_expect_functions.tcl index 6c91d5574d9..592e3d9bd94 100755 --- a/integration-tests/bats/helper/common_expect_functions.tcl +++ b/integration-tests/bats/helper/common_expect_functions.tcl @@ -60,3 +60,21 @@ proc expect_with_defaults_2 {patternA patternB action} { } } +proc expect_without_pattern {bad_pattern action} { + expect { + -re $bad_pattern { + puts "ERROR: Found unexpected pattern: $bad_pattern" + exit 1 + } + timeout { + eval $action + } + eof { + eval $action + } + failed { + puts "<>" + exit 1 + } + } +} diff --git a/integration-tests/bats/sql-shell-binary-as-hex.expect b/integration-tests/bats/sql-shell-binary-as-hex.expect new file mode 100644 index 00000000000..81a97b5c516 --- /dev/null +++ b/integration-tests/bats/sql-shell-binary-as-hex.expect @@ -0,0 +1,102 @@ +#!/usr/bin/expect +# dolthub/dolt#9554 +# https://github.com/dolthub/dolt/issues/9554 +# Test script for binary-a-hex flag behavior in dolt sql. +# +# Usage: +# expect binary-hex-test.expect [flags...] +# +# Tracked flags: +# --binary-as-hex: Use binary as hex encoding for VARBINARY and BINARY types. +# --skip-binary-as-hex: Skip binary as hex encoding for VARBINARY and BINARY types. + +source "$env(BATS_CWD)/helper/common_expect_functions.tcl" + +set timeout 10 +set env(NO_COLOR) 1 + +set has_binary_hex 0 +set has_skip_hex 0 + +foreach arg $argv { + if {$arg eq "--binary-as-hex"} {set has_binary_hex 1} + if {$arg eq "--skip-binary-as-hex"} {set has_skip_hex 1} +} + +# In the interactive shell, the default behavior is to use binary as hex output. +if {!$has_skip_hex} { + set has_binary_hex 1 +} + +proc run_query {query expect_proc} { + global has_skip_hex has_binary_hex argv + expect_with_defaults {>} "send {$query\r}" + eval $expect_proc +} + +# Handles the following cases: +# 1. check dolt's ability to detect conflicting flags. +# 2. spawns interactive shell +spawn dolt sql {*}$argv + +if {$has_binary_hex && $has_skip_hex} { + expect { + "cannot use both --binary-as-hex and --skip-binary-as-hex" { + expect eof + exit 3 # differentiate exit err from common_expect_functions.tcl + } + eof { + puts "Process ended without error message." + exit 1 + } + } +} + +run_query "DROP TABLE IF EXISTS test_vbin;" {} +run_query "CREATE TABLE test_vbin (id INT PRIMARY KEY, v VARBINARY(10));" {} +run_query "INSERT INTO test_vbin VALUES (1, 'abc');" {} +run_query "SELECT *, LENGTH(v) FROM test_vbin;" { + if {$has_skip_hex} { + expect_without_pattern {0x[0-9A-F]+} {} + } else { + expect_with_defaults_2 {0x616263} {\| 3 } {} + } +} + +run_query "INSERT INTO test_vbin VALUES (2, UNHEX('0A000000001000112233'));" {} +run_query "INSERT INTO test_vbin VALUES (3, UNHEX(''));" {} +run_query "SELECT *, LENGTH(v) FROM test_vbin;" { + if {$has_skip_hex} { + expect_without_pattern {0x[0-9A-F]+} {} + } else { + expect_with_defaults_2 {0x616263} {\| 3 } {} + expect_with_defaults_2 {0x0A000000001000112233} {\| 10 } {} + expect_with_defaults_2 {0x} {\| 0 } {} + } +} + +run_query "DROP TABLE IF EXISTS test_bin;" {} +run_query "CREATE TABLE test_bin (id INT PRIMARY KEY, b BINARY(10));" {} +run_query "INSERT INTO test_bin VALUES (1, 'abc');" {} +run_query "SELECT *, LENGTH(b) FROM test_bin;" { + if {$has_skip_hex} { + expect_without_pattern {0x[0-9A-F]+} {} + } else { + expect_with_defaults_2 {0x61626300000000000000} {\| 10 } {} + } +} + +run_query "INSERT INTO test_bin VALUES (2, UNHEX('0A000000001000112233'));" {} +run_query "INSERT INTO test_bin VALUES (3, UNHEX(''));" {} +run_query "SELECT *, LENGTH(b) FROM test_bin;" { + if {$has_skip_hex} { + expect_without_pattern {0x[0-9A-F]+} {} + } else { + expect_with_defaults_2 {0x61626300000000000000} {\| 10 } {} + expect_with_defaults_2 {0x0A000000001000112233} {\| 10 } {} + expect_with_defaults_2 {0x00000000000000000000} {\| 10 } {} + } +} + +run_query "exit;" { expect eof } +exit 0 diff --git a/integration-tests/bats/sql-shell.bats b/integration-tests/bats/sql-shell.bats index 4f51eb43c24..e14534c3816 100644 --- a/integration-tests/bats/sql-shell.bats +++ b/integration-tests/bats/sql-shell.bats @@ -1069,4 +1069,70 @@ expect eof run $BATS_TEST_DIRNAME/sql-shell-commit-time.expect [ "$status" -eq 0 ] +} + +# bats test_tags=no_lambda +@test "sql-shell: -binary-as-hex, -skip-binary-as-hex flag is respected in server and local contexts" { + skiponwindows "Missing Dependencies" + + # Default behavior for interactive runs is to output binary as hex + run expect "$BATS_TEST_DIRNAME"/sql-shell-binary-as-hex.expect + [ "$status" -eq 0 ] + + run expect "$BATS_TEST_DIRNAME"/sql-shell-binary-as-hex.expect --binary-as-hex + [ "$status" -eq 0 ] + + run expect "$BATS_TEST_DIRNAME"/sql-shell-binary-as-hex.expect --skip-binary-as-hex + [ "$status" -eq 0 ] + + run expect "$BATS_TEST_DIRNAME"/sql-shell-binary-as-hex.expect --binary-as-hex --skip-binary-as-hex + [ "$status" -eq 3 ] + + # Non-interactive runs should not output binary as hex by default + run dolt sql -q "SELECT * FROM test_vbin" + [ "$status" -eq 0 ] + [[ ! $output =~ 0x[0-9A-F]+ ]] || false + + run dolt sql -q "SELECT * FROM test_bin" + [ "$status" -eq 0 ] + [[ ! $output =~ 0x[0-9A-F]+ ]] || false + + run dolt sql -q "SELECT * FROM test_vbin" --binary-as-hex + [ "$status" -eq 0 ] + [[ $output =~ 1.*0x616263 ]] || false + [[ $output =~ 2.*0x0A000000001000112233 ]] || false + [[ $output =~ 3.*0x ]] || false + + run dolt sql -q "SELECT * FROM test_bin" --binary-as-hex + [ $status -eq 0 ] + [[ $output =~ 1.*0x61626300000000000000 ]] || false + [[ $output =~ 2.*0x0A000000001000112233 ]] || false + [[ $output =~ 3.*0x00000000000000000000 ]] || false + + run dolt sql -q "SELECT * FROM test_vbin" --skip-binary-as-hex + [ "$status" -eq 0 ] + [[ ! $output =~ 0x[0-9A-F]+ ]] || false + + run dolt sql -q "SELECT * FROM test_bin" --skip-binary-as-hex + [ "$status" -eq 0 ] + [[ ! $output =~ 0x[0-9A-F]+ ]] || false + + run dolt sql -q "" --binary-as-hex --skip-binary-as-hex + [ "$status" -eq 1 ] + [[ "$output" =~ "cannot use both --binary-as-hex and --skip-binary-as-hex" ]] || false + + # Check other formats output is correct + run dolt sql -r csv -q "SELECT * FROM test_vbin WHERE id = 1;" --binary-as-hex + [ "$status" -eq 0 ] + [[ "$output" =~ "1,0x616263" ]] || false + + run dolt sql -r csv -q "SELECT * FROM test_vbin WHERE id = 1;" + [ "$status" -eq 0 ] + [[ "$output" =~ "1,abc" ]] || false + + run dolt sql -r csv -q "SELECT * FROM test_bin" --binary-as-hex + [ "$status" -eq 0 ] + [[ "$output" =~ "1,0x61626300000000000000" ]] || false + [[ "$output" =~ "2,0x0A000000001000112233" ]] || false + [[ "$output" =~ "3,0x00000000000000000000" ]] || false } \ No newline at end of file