diff --git a/go/cmd/dolt/commands/sql.go b/go/cmd/dolt/commands/sql.go index 4a1101d562e..d2baea5b152 100644 --- a/go/cmd/dolt/commands/sql.go +++ b/go/cmd/dolt/commands/sql.go @@ -719,6 +719,9 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu } verticalOutputLineTerminators := []string{"\\g", "\\G"} + // clearStatementTerminator mirrors the MySQL client's `\c` escape: it discards the statement + // currently being entered (however many lines it spans) without executing it. + clearStatementTerminator := "\\c" backSlashCommands := make([]string, 0, len(slashCmds)) for _, cmd := range slashCmds { backSlashCommands = append(backSlashCommands, "\\"+cmd.Name()) @@ -730,7 +733,7 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu "quit", "exit", "quit()", "exit()", }, LineTerminator: ";", - SpecialTerminators: verticalOutputLineTerminators, + SpecialTerminators: append(append([]string{}, verticalOutputLineTerminators...), clearStatementTerminator), BackSlashCmds: backSlashCommands, } @@ -783,6 +786,16 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu return } + // \c cancels the statement currently being entered, matching the MySQL client. The buffered + // input is discarded without being executed or recorded in history, and the shell resets to + // a fresh prompt. + if strings.HasSuffix(query, clearStatementTerminator) { + nextPrompt, multiPrompt := postCommandUpdate(sqlCtx, qryist) + shell.SetPrompt(nextPrompt) + shell.SetMultiPrompt(multiPrompt) + return + } + trackHistory(shell, query) query = strings.TrimSuffix(query, shell.LineTerminator()) diff --git a/integration-tests/bats/sql-shell-clear-statement.expect b/integration-tests/bats/sql-shell-clear-statement.expect new file mode 100755 index 00000000000..116cfe5ebfb --- /dev/null +++ b/integration-tests/bats/sql-shell-clear-statement.expect @@ -0,0 +1,26 @@ +#!/usr/bin/expect + +set timeout 5 +set env(NO_COLOR) 1 + +source "$env(BATS_CWD)/helper/common_expect_functions.tcl" + +spawn dolt sql + +# \c cancels the statement currently being entered, mirroring the MySQL client's \c escape. +# The buffered statement should be discarded without being executed. If \c were left +# unhandled, the shell would fold it into the next line typed, producing a syntax error near +# the stray backslash instead of running "select 1;" on its own -- see the bats assertions in +# sql-shell.bats for how this is verified against the full transcript. +expect_with_defaults {dolt-repo-[0-9]+/main\*?> } { send "select * from nonexistent_table \\c\r" } + +# Give the shell a moment to process (or mis-process) the \c before sending the next +# statement, rather than racing to match a pattern that could also appear in echoed input. +sleep 1 + +send "select 1;\r" +sleep 1 + +send "quit\r" + +expect eof diff --git a/integration-tests/bats/sql-shell.bats b/integration-tests/bats/sql-shell.bats index 5004d18a874..58afaffe30b 100644 --- a/integration-tests/bats/sql-shell.bats +++ b/integration-tests/bats/sql-shell.bats @@ -155,6 +155,23 @@ teardown() { [ "$status" -eq 0 ] } +# Regression coverage for https://github.com/dolthub/dolt/issues/10867 +# bats test_tags=no_lambda +@test "sql-shell: \\c cancels the statement currently being entered" { + skiponwindows "Need to install expect and make this script work on windows." + if [ "$SQL_ENGINE" = "remote-engine" ]; then + skip "Current test setup results in remote calls having a clean branch, where this expect script expects dirty." + fi + run $BATS_TEST_DIRNAME/sql-shell-clear-statement.expect + echo "$output" + + [ "$status" -eq 0 ] + # \c must not have been folded into the next statement (a bug would surface as a syntax + # error near the stray backslash), and the next statement must have actually executed. + [[ ! "$output" =~ "syntax error" ]] || false + [[ "$output" =~ "1 row in set" ]] || false +} + # Regression coverage for https://github.com/dolthub/dolt/issues/11137 # bats test_tags=no_lambda @test "sql-shell: status slash command works without --branch flag" {