From f8c301784cb3a180e3f11fcccc4d4d00f4e713f7 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 25 Sep 2020 15:34:52 +0200 Subject: [PATCH 1/5] cli/sql: polish the output of `\?` Release note (cli change): The help text displayed upon `\?` in `cockroach sql` / `cockroach demo` now groups the recognized client-side commands into sections for easier reading. --- pkg/cli/sql.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index 3840ab7b5aea..a8d2ab4db2aa 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -51,21 +51,32 @@ const ( # ` helpMessageFmt = `You are using 'cockroach sql', CockroachDB's lightweight SQL client. -Type: - \? or "help" print this help. +General \q, quit, exit exit the shell (Ctrl+C/Ctrl+D also supported). - \! CMD run an external command and print its results on standard output. - \| CMD run an external command and run its output as SQL statements. - \set [NAME] set a client-side flag or (without argument) print the current settings. - \unset NAME unset a flag. - \show during a multi-line statement or transaction, show the SQL entered so far. + +Help + \? or "help" print this help. \h [NAME] help on syntax of SQL commands. \hf [NAME] help on SQL built-in functions. + +Query Buffer + \show during a multi-line statement, show the SQL entered so far. + \| CMD run an external command and run its output as SQL statements. + +Informational \l list all databases in the CockroachDB cluster. \dt show the tables of the current schema in the current database. \dT show the user defined types of the current database. \du list the users for all databases. \d [TABLE] show details about columns in the specified table, or alias for '\dt' if no table is specified. + +Operating System + \! CMD run an external command and print its results on standard output. + +Configuration + \set [NAME] set a client-side flag or (without argument) print the current settings. + \unset NAME unset a flag. + %s More documentation about our SQL dialect and the CLI shell is available online: %s From 2deb0381acc96108d406db76b710b9c603b8ca6d Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 25 Sep 2020 15:37:22 +0200 Subject: [PATCH 2/5] cli/sql: deprecate `\show`; implement `\p` and `\r` Release note (cli change): The client-side command `\show` for the SQL shell is deprecated in favor of the new command `\p`. This prints the contents of the query buffer entered so far. Release note (cli change): The new client-side command `\r` for the SQL shell erases the contents of the query buffer entered so far. This provides a convenient way to reset the input e.g. when the user gets themselves confused with string delimiters. --- .../test_multiline_statements.tcl | 19 +++++++++++++++---- pkg/cli/sql.go | 14 +++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pkg/cli/interactive_tests/test_multiline_statements.tcl b/pkg/cli/interactive_tests/test_multiline_statements.tcl index 35365a79890f..2cade7edf5b7 100644 --- a/pkg/cli/interactive_tests/test_multiline_statements.tcl +++ b/pkg/cli/interactive_tests/test_multiline_statements.tcl @@ -30,8 +30,8 @@ send "2, 3\r" eexpect " ->" end_test -start_test "Test that \show does what it says." -send "\\show\r" +start_test "Test that \p does what it says." +send "\\p\r" eexpect "select 1," eexpect "2, 3" eexpect " ->" @@ -49,6 +49,17 @@ eexpect "2, 3" eexpect ";" end_test +start_test "Test that \r does what it says." +# backspace to erase the semicolon +send "\010" +# newline to get a prompt +send "\r" +eexpect " ->" +# Now send \r followed by a carriage return. +send "\\r\r" +eexpect root@ +end_test + start_test "Test that Ctrl+C after the first line merely cancels the statement and presents the prompt." send "\r" eexpect root@ @@ -58,10 +69,10 @@ interrupt eexpect root@ end_test -start_test "Test that \show does what it says." +start_test "Test that \p does what it says." send "select\r" eexpect " ->" -send "\\show\r" +send "\\p\r" eexpect "select\r\n*->" interrupt end_test diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index a8d2ab4db2aa..2264dc125e92 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -60,7 +60,8 @@ Help \hf [NAME] help on SQL built-in functions. Query Buffer - \show during a multi-line statement, show the SQL entered so far. + \p during a multi-line statement, show the SQL entered so far. + \r during a multi-line statement, erase all the SQL entered so far. \| CMD run an external command and run its output as SQL statements. Informational @@ -1045,7 +1046,18 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu case `\!`: return c.runSyscmd(c.lastInputLine, loopState, errState) + case `\p`: + // This is analogous to \show but does not need a special case. + // Implemented for compatibility with psql. + fmt.Println(strings.Join(c.partialLines, "\n")) + + case `\r`: + // Reset the input buffer so far. This is useful when e.g. a user + // got confused with string delimiters and multi-line input. + return cliStartLine + case `\show`: + fmt.Fprintln(stderr, `warning: \show is deprecated. Use \p.`) if len(c.partialLines) == 0 { fmt.Fprintf(stderr, "No input so far. Did you mean SHOW?\n") } else { From dab759066d04cc3b3f70c79714a891f9859245da Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 25 Sep 2020 16:18:31 +0200 Subject: [PATCH 3/5] cli/sql: move configurable variables to `sqlCtx` Ahead of implementing recursive evaluation, we need to ensure that all variables customizable via `\set` / `\unset` become independent of `cliState`. This patch does it. Release note: None --- pkg/cli/context.go | 19 +++++- pkg/cli/sql.go | 146 +++++++++++++++++++++----------------------- pkg/cli/sql_test.go | 3 + 3 files changed, 90 insertions(+), 78 deletions(-) diff --git a/pkg/cli/context.go b/pkg/cli/context.go index 554dea0fd0c3..131e70260bb0 100644 --- a/pkg/cli/context.go +++ b/pkg/cli/context.go @@ -195,7 +195,7 @@ func setCliContextDefaults() { cliCtx.allowUnencryptedClientPassword = false } -// sqlCtx captures the command-line parameters of the `sql` command. +// sqlCtx captures the configuration of the `sql` command. // See below for defaults. var sqlCtx = struct { *cliContext @@ -238,6 +238,19 @@ var sqlCtx = struct { // Determine whether to show raw durations. verboseTimings bool + + // Determines whether to stop the client upon encountering an error. + errExit bool + + // Determines whether to perform client-side syntax checking. + checkSyntax bool + + // autoTrace, when non-empty, encloses the executed statements + // by suitable SET TRACING and SHOW TRACE FOR SESSION statements. + autoTrace string + + // The string used to produce the value of fullPrompt. + customPromptPattern string }{cliContext: &cliCtx} // setSQLContextDefaults set the default values in sqlCtx. This @@ -254,6 +267,10 @@ func setSQLContextDefaults() { sqlCtx.echo = false sqlCtx.enableServerExecutionTimings = false sqlCtx.verboseTimings = false + sqlCtx.errExit = false + sqlCtx.checkSyntax = false + sqlCtx.autoTrace = "" + sqlCtx.customPromptPattern = defaultPromptPattern } // zipCtx captures the command-line parameters of the `zip` command. diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index 2264dc125e92..381933a83d74 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -111,6 +111,11 @@ Open a sql shell running against a cockroach database. // cliState defines the current state of the CLI during // command-line processing. +// +// Note: options customizable via \set and \unset should be defined in +// sqlCtx or cliCtx instead, so that the configuration remains globals +// across multiple instances of cliState (e.g. across file inclusion +// with \i). type cliState struct { conn *sqlConn // ins is used to read lines if isInteractive is true. @@ -118,13 +123,6 @@ type cliState struct { // buf is used to read lines if isInteractive is false. buf *bufio.Reader - // Options - // - // Determines whether to stop the client upon encountering an error. - errExit bool - // Determines whether to perform client-side syntax checking. - checkSyntax bool - // The prompt at the beginning of a multi-line entry. fullPrompt string // The prompt on a continuation line in a multi-line entry. @@ -133,8 +131,6 @@ type cliState struct { useContinuePrompt bool // The current prompt, either fullPrompt or continuePrompt. currentPrompt string - // The string used to produce the value of fullPrompt. - customPromptPattern string // State // @@ -178,10 +174,6 @@ type cliState struct { // by Ctrl+D, causes the shell to terminate with an error -- // reporting the status of the last valid SQL statement executed. exitErr error - - // autoTrace, when non-empty, encloses the executed statements - // by suitable SET TRACING and SHOW TRACE FOR SESSION statements. - autoTrace string } // cliStateEnum drives the CLI state machine in runInteractive(). @@ -292,48 +284,48 @@ var options = map[string]struct { description string isBoolean bool validDuringMultilineEntry bool - set func(c *cliState, val string) error - reset func(c *cliState) error + set func(val string) error + reset func() error // display is used to retrieve the current value. - display func(c *cliState) string + display func() string deprecated bool }{ `auto_trace`: { description: "automatically run statement tracing on each executed statement", isBoolean: false, validDuringMultilineEntry: false, - set: func(c *cliState, val string) error { + set: func(val string) error { val = strings.ToLower(strings.TrimSpace(val)) switch val { case "false", "0", "off": - c.autoTrace = "" + sqlCtx.autoTrace = "" case "true", "1": val = "on" fallthrough default: - c.autoTrace = "on, " + val + sqlCtx.autoTrace = "on, " + val } return nil }, - reset: func(c *cliState) error { - c.autoTrace = "" + reset: func() error { + sqlCtx.autoTrace = "" return nil }, - display: func(c *cliState) string { - if c.autoTrace == "" { + display: func() string { + if sqlCtx.autoTrace == "" { return "off" } - return c.autoTrace + return sqlCtx.autoTrace }, }, `display_format`: { description: "the output format for tabular data (table, csv, tsv, html, sql, records, raw)", isBoolean: false, validDuringMultilineEntry: true, - set: func(_ *cliState, val string) error { + set: func(val string) error { return cliCtx.tableDisplayFormat.Set(val) }, - reset: func(_ *cliState) error { + reset: func() error { displayFormat := tableDisplayTSV if cliCtx.terminalOutput { displayFormat = tableDisplayTable @@ -341,78 +333,78 @@ var options = map[string]struct { cliCtx.tableDisplayFormat = displayFormat return nil }, - display: func(_ *cliState) string { return cliCtx.tableDisplayFormat.String() }, + display: func() string { return cliCtx.tableDisplayFormat.String() }, }, `echo`: { description: "show SQL queries before they are sent to the server", isBoolean: true, validDuringMultilineEntry: false, - set: func(_ *cliState, _ string) error { sqlCtx.echo = true; return nil }, - reset: func(_ *cliState) error { sqlCtx.echo = false; return nil }, - display: func(_ *cliState) string { return strconv.FormatBool(sqlCtx.echo) }, + set: func(_ string) error { sqlCtx.echo = true; return nil }, + reset: func() error { sqlCtx.echo = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.echo) }, }, `errexit`: { description: "exit the shell upon a query error", isBoolean: true, validDuringMultilineEntry: true, - set: func(c *cliState, _ string) error { c.errExit = true; return nil }, - reset: func(c *cliState) error { c.errExit = false; return nil }, - display: func(c *cliState) string { return strconv.FormatBool(c.errExit) }, + set: func(_ string) error { sqlCtx.errExit = true; return nil }, + reset: func() error { sqlCtx.errExit = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.errExit) }, }, `check_syntax`: { description: "check the SQL syntax before running a query", isBoolean: true, validDuringMultilineEntry: false, - set: func(c *cliState, _ string) error { c.checkSyntax = true; return nil }, - reset: func(c *cliState) error { c.checkSyntax = false; return nil }, - display: func(c *cliState) string { return strconv.FormatBool(c.checkSyntax) }, + set: func(_ string) error { sqlCtx.checkSyntax = true; return nil }, + reset: func() error { sqlCtx.checkSyntax = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.checkSyntax) }, }, `show_times`: { description: "display the execution time after each query", isBoolean: true, validDuringMultilineEntry: true, - set: func(_ *cliState, _ string) error { sqlCtx.showTimes = true; return nil }, - reset: func(_ *cliState) error { sqlCtx.showTimes = false; return nil }, - display: func(_ *cliState) string { return strconv.FormatBool(sqlCtx.showTimes) }, + set: func(_ string) error { sqlCtx.showTimes = true; return nil }, + reset: func() error { sqlCtx.showTimes = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.showTimes) }, }, `show_server_times`: { description: "display the server execution times for queries (requires show_times to be set)", isBoolean: true, validDuringMultilineEntry: true, - set: func(_ *cliState, _ string) error { sqlCtx.enableServerExecutionTimings = true; return nil }, - reset: func(_ *cliState) error { sqlCtx.enableServerExecutionTimings = false; return nil }, - display: func(_ *cliState) string { return strconv.FormatBool(sqlCtx.enableServerExecutionTimings) }, + set: func(_ string) error { sqlCtx.enableServerExecutionTimings = true; return nil }, + reset: func() error { sqlCtx.enableServerExecutionTimings = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.enableServerExecutionTimings) }, }, `verbose_times`: { description: "display execution times with more precision (requires show_times to be set)", isBoolean: true, validDuringMultilineEntry: true, - set: func(_ *cliState, _ string) error { sqlCtx.verboseTimings = true; return nil }, - reset: func(_ *cliState) error { sqlCtx.verboseTimings = false; return nil }, - display: func(_ *cliState) string { return strconv.FormatBool(sqlCtx.verboseTimings) }, + set: func(_ string) error { sqlCtx.verboseTimings = true; return nil }, + reset: func() error { sqlCtx.verboseTimings = false; return nil }, + display: func() string { return strconv.FormatBool(sqlCtx.verboseTimings) }, }, `smart_prompt`: { description: "deprecated", isBoolean: true, validDuringMultilineEntry: false, - set: func(c *cliState, _ string) error { return nil }, - reset: func(c *cliState) error { return nil }, - display: func(c *cliState) string { return "false" }, + set: func(_ string) error { return nil }, + reset: func() error { return nil }, + display: func() string { return "false" }, deprecated: true, }, `prompt1`: { description: "prompt string to use before each command (the following are expanded: %M full host, %m host, %> port number, %n user, %/ database, %x txn status)", isBoolean: false, validDuringMultilineEntry: true, - set: func(c *cliState, val string) error { - c.customPromptPattern = val + set: func(val string) error { + sqlCtx.customPromptPattern = val return nil }, - reset: func(c *cliState) error { - c.customPromptPattern = defaultPromptPattern + reset: func() error { + sqlCtx.customPromptPattern = defaultPromptPattern return nil }, - display: func(c *cliState) string { return c.customPromptPattern }, + display: func() string { return sqlCtx.customPromptPattern }, }, } @@ -435,7 +427,7 @@ func (c *cliState) handleSet(args []string, nextState, errState cliStateEnum) cl if options[n].deprecated { continue } - optData = append(optData, []string{n, options[n].display(c), options[n].description}) + optData = append(optData, []string{n, options[n].display(), options[n].description}) } err := printQueryOutput(os.Stdout, []string{"Option", "Value", "Description"}, @@ -474,13 +466,13 @@ func (c *cliState) handleSet(args []string, nextState, errState cliStateEnum) cl // Run the command. var err error if !opt.isBoolean { - err = opt.set(c, val) + err = opt.set(val) } else { switch val { case "true", "1", "on": - err = opt.set(c, "true") + err = opt.set("true") case "false", "0", "off": - err = opt.reset(c) + err = opt.reset() default: return c.invalidOptSet(errState, args) } @@ -506,7 +498,7 @@ func (c *cliState) handleUnset(args []string, nextState, errState cliStateEnum) if len(c.partialLines) > 0 && !opt.validDuringMultilineEntry { return c.invalidOptionChange(errState, args[0]) } - if err := opt.reset(c); err != nil { + if err := opt.reset(); err != nil { fmt.Fprintf(stderr, "\\unset %s: %v\n", args[0], err) return errState } @@ -714,7 +706,7 @@ func (c *cliState) doRefreshPrompts(nextState cliStateEnum) cliStateEnum { dbName := unknownDbName c.lastKnownTxnStatus = unknownTxnStatus - wantDbStateInPrompt := rePromptDbState.MatchString(c.customPromptPattern) + wantDbStateInPrompt := rePromptDbState.MatchString(sqlCtx.customPromptPattern) if wantDbStateInPrompt { c.refreshTransactionStatus() // refreshDatabaseName() must be called *after* refreshTransactionStatus(), @@ -724,7 +716,7 @@ func (c *cliState) doRefreshPrompts(nextState cliStateEnum) cliStateEnum { dbName = c.refreshDatabaseName() } - c.fullPrompt = rePromptFmt.ReplaceAllStringFunc(c.customPromptPattern, func(m string) string { + c.fullPrompt = rePromptFmt.ReplaceAllStringFunc(sqlCtx.customPromptPattern, func(m string) string { switch m { case "%M": return parsedURL.Host // full host name. @@ -1015,7 +1007,7 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu } errState := loopState - if c.errExit { + if sqlCtx.errExit { // If exiterr is set, an error in a client-side command also // terminates the shell. errState = cliStop @@ -1164,7 +1156,7 @@ func (c *cliState) doPrepareStatementLine( // Complete input. Remember it in the history. c.addHistory(c.concatLines) - if !c.checkSyntax { + if !sqlCtx.checkSyntax { return execState } @@ -1184,7 +1176,7 @@ func (c *cliState) doCheckStatement(startState, contState, execState cliStateEnu &formattedError{err: err, showSeverity: false, verbose: false}) // Stop here if exiterr is set. - if c.errExit { + if sqlCtx.errExit { return cliStop } @@ -1216,13 +1208,13 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum { c.lastKnownTxnStatus = " ?" // Are we tracing? - if c.autoTrace != "" { + if sqlCtx.autoTrace != "" { // Clear the trace by disabling tracing, then restart tracing // with the specified options. - c.exitErr = c.conn.Exec("SET tracing = off; SET tracing = "+c.autoTrace, nil) + c.exitErr = c.conn.Exec("SET tracing = off; SET tracing = "+sqlCtx.autoTrace, nil) if c.exitErr != nil { cliOutputError(stderr, c.exitErr, true /*showSeverity*/, false /*verbose*/) - if c.errExit { + if sqlCtx.errExit { return cliStop } return nextState @@ -1237,7 +1229,7 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum { // If we are tracing, stop tracing and display the trace. We do // this even if there was an error: a trace on errors is useful. - if c.autoTrace != "" { + if sqlCtx.autoTrace != "" { // First, disable tracing. if err := c.conn.Exec("SET tracing = off", nil); err != nil { // Print the error for the SET tracing statement. This will @@ -1254,7 +1246,7 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum { // shell. } else { traceType := "" - if strings.Contains(c.autoTrace, "kv") { + if strings.Contains(sqlCtx.autoTrace, "kv") { traceType = "kv" } if err := runQueryAndFormatResults(c.conn, os.Stdout, @@ -1273,7 +1265,7 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum { } } - if c.exitErr != nil && c.errExit { + if c.exitErr != nil && sqlCtx.errExit { return cliStop } @@ -1373,18 +1365,18 @@ func (c *cliState) configurePreShellDefaults(cmdIn *os.File) (cleanupFn func(), if cliCtx.isInteractive { // If a human user is providing the input, we want to help them with // what they are entering: - c.errExit = false // let the user retry failing commands + sqlCtx.errExit = false // let the user retry failing commands if !sqlCtx.debugMode { // Also, try to enable syntax checking if supported by the server. // This is a form of client-side error checking to help with large txns. - c.checkSyntax = true + sqlCtx.checkSyntax = true } } else { // When running non-interactive, by default we want errors to stop // further processing and we can just let syntax checking to be // done server-side to avoid client-side churn. - c.errExit = true - c.checkSyntax = false + sqlCtx.errExit = true + sqlCtx.checkSyntax = false // We also don't need (smart) prompts at all. } @@ -1425,9 +1417,9 @@ func (c *cliState) configurePreShellDefaults(cmdIn *os.File) (cleanupFn func(), // command-line client. // Default prompt is part of the connection URL. eg: "marc@localhost:26257>". - c.customPromptPattern = defaultPromptPattern + sqlCtx.customPromptPattern = defaultPromptPattern if sqlCtx.debugMode { - c.customPromptPattern = debugPromptPattern + sqlCtx.customPromptPattern = debugPromptPattern } c.ins.SetCompleter(c) @@ -1471,11 +1463,11 @@ func (c *cliState) runStatements(stmts []string) error { // we are returning directly. c.exitErr = runQueryAndFormatResults(c.conn, os.Stdout, makeQuery(stmt)) if c.exitErr != nil { - if !c.errExit && i < len(stmts)-1 { + if !sqlCtx.errExit && i < len(stmts)-1 { // Print the error now because we don't get a chance later. cliOutputError(stderr, c.exitErr, true /*showSeverity*/, false /*verbose*/) } - if c.errExit { + if sqlCtx.errExit { break } } diff --git a/pkg/cli/sql_test.go b/pkg/cli/sql_test.go index d1674fd4b21a..1d432bb6c134 100644 --- a/pkg/cli/sql_test.go +++ b/pkg/cli/sql_test.go @@ -191,6 +191,8 @@ func TestIsEndOfStatement(t *testing.T) { func TestHandleCliCmdSqlAlias(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) + initCLIDefaults() + clientSideCommandTestsTable := []struct { commandString string wantSQLStmt string @@ -217,6 +219,7 @@ func TestHandleCliCmdSqlAlias(t *testing.T) { func TestHandleCliCmdSlashDInvalidSyntax(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) + initCLIDefaults() clientSideCommandTests := []string{`\d goodarg badarg`, `\dz`} From 24b4fe1a52da97a3e6fa40035ff90474b68d74c4 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 25 Sep 2020 16:26:30 +0200 Subject: [PATCH 4/5] cli/sql: new local command `\echo` Release note (cli change): The SQL shell (`cockroach sql`, `cockroach demo`) now supports the client-side command `\echo`, like `psql`. This can be used e.g. to generate informational output when executing SQL scripts non-interactively. --- pkg/cli/interactive_tests/test_local_cmds.tcl | 12 ++++++++++++ pkg/cli/sql.go | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/pkg/cli/interactive_tests/test_local_cmds.tcl b/pkg/cli/interactive_tests/test_local_cmds.tcl index 25c908e15d8e..7e042b2184a0 100755 --- a/pkg/cli/interactive_tests/test_local_cmds.tcl +++ b/pkg/cli/interactive_tests/test_local_cmds.tcl @@ -221,6 +221,18 @@ eexpect "with no argument" eexpect root@ end_test +start_test "Check that \\echo behaves well." +send "\\echo\r" +eexpect "\r\n" +eexpect "\r\n" +eexpect root@ + +send "\\echo hello world\r" +# echo removes double spaces within the line. That's expected. +eexpect "hello world" +eexpect root@ +end_test + start_test "Check that commands are also recognized with a final semicolon." send "\\set;\r" eexpect "display_format" diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index 381933a83d74..6062b72c78a1 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -64,6 +64,9 @@ Query Buffer \r during a multi-line statement, erase all the SQL entered so far. \| CMD run an external command and run its output as SQL statements. +Input/Output + \echo [STRING] write the provided string to standard output. + Informational \l list all databases in the CockroachDB cluster. \dt show the tables of the current schema in the current database. @@ -1029,6 +1032,9 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu case `\`, `\?`, `\help`: c.printCliHelp() + case `\echo`: + fmt.Println(strings.Join(cmd[1:], " ")) + case `\set`: return c.handleSet(cmd[1:], loopState, errState) From 88939ce4ba9e2e93d5b5976df29babf37a1301c6 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 25 Sep 2020 17:07:27 +0200 Subject: [PATCH 5/5] cli/sql: implement the `\i` and `\ir` client-side command Release note (cli change): The SQL shell (`cockroach sql`, `cockroach demo`) now support the `\i` and `\ir` client-side command which reads SQL file and evaluates its content in-place. `\ir` differs from `\i` in that the file name is resolved relative to the location of the script containing the `\ir` command. This makes `\ir` likely more desirable in the general case. Occurences of `\q` inside a file included via `\i`/`\ir stop evaluation of the file and resume evaluation of the file that included it. This feature is compatible with the identically named `psql` commands. It is meant to help compose complex initialization scripts from a library of standard components. For example, one could be defining each table and its initial contents in separate SQL files, and then use different super-files to include different tables depending on the desired final schema. --- pkg/cli/cli_test.go | 43 +++++++++++++++ pkg/cli/sql.go | 82 ++++++++++++++++++++++++++++- pkg/cli/testdata/i_maxrecursion.sql | 1 + pkg/cli/testdata/i_multiline.sql | 3 ++ pkg/cli/testdata/i_stopmiddle.sql | 3 ++ pkg/cli/testdata/i_twolevels1.sql | 9 ++++ pkg/cli/testdata/i_twolevels2.sql | 7 +++ pkg/cli/testdata/i_twolevels3.sql | 1 + 8 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 pkg/cli/testdata/i_maxrecursion.sql create mode 100644 pkg/cli/testdata/i_multiline.sql create mode 100644 pkg/cli/testdata/i_stopmiddle.sql create mode 100644 pkg/cli/testdata/i_twolevels1.sql create mode 100644 pkg/cli/testdata/i_twolevels2.sql create mode 100644 pkg/cli/testdata/i_twolevels3.sql diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index f6f878e71c73..aa00162fc400 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -2089,3 +2089,46 @@ func Example_read_from_file() { // ERROR: column "undefined" does not exist // SQLSTATE: 42703 } + +// Example_includes tests the \i command. +func Example_includes() { + c := newCLITest(cliTestParams{}) + defer c.cleanup() + + c.RunWithArgs([]string{"sql", "-f", "testdata/i_twolevels1.sql"}) + c.RunWithArgs([]string{"sql", "-f", "testdata/i_multiline.sql"}) + c.RunWithArgs([]string{"sql", "-f", "testdata/i_stopmiddle.sql"}) + c.RunWithArgs([]string{"sql", "-f", "testdata/i_maxrecursion.sql"}) + + // Output: + // sql -f testdata/i_twolevels1.sql + // > SELECT 123; + // ?column? + // 123 + // > SELECT 789; + // ?column? + // 789 + // ?column? + // 456 + // sql -f testdata/i_multiline.sql + // ERROR: at or near "\": syntax error + // SQLSTATE: 42601 + // DETAIL: source SQL: + // SELECT -- incomplete statement, \i invalid + // \i testdata/i_twolevels2.sql + // ^ + // HINT: try \h SELECT + // ERROR: at or near "\": syntax error + // SQLSTATE: 42601 + // DETAIL: source SQL: + // SELECT -- incomplete statement, \i invalid + // \i testdata/i_twolevels2.sql + // ^ + // HINT: try \h SELECT + // sql -f testdata/i_stopmiddle.sql + // ?column? + // 123 + // sql -f testdata/i_maxrecursion.sql + // \i: too many recursion levels (max 10) + // ERROR: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: testdata/i_maxrecursion.sql: \i: too many recursion levels (max 10) +} diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index 6062b72c78a1..54d0f9e8a6dc 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -66,6 +66,8 @@ Query Buffer Input/Output \echo [STRING] write the provided string to standard output. + \i execute commands from the specified file. + \ir as \i, but relative to the location of the current script. Informational \l list all databases in the CockroachDB cluster. @@ -126,6 +128,12 @@ type cliState struct { // buf is used to read lines if isInteractive is false. buf *bufio.Reader + // levels is the number of inclusion recursion levels. + levels int + // includeDir is the directory relative to which relative + // includes (\ir) resolve the file name. + includeDir string + // The prompt at the beginning of a multi-line entry. fullPrompt string // The prompt on a continuation line in a multi-line entry. @@ -1044,6 +1052,12 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu case `\!`: return c.runSyscmd(c.lastInputLine, loopState, errState) + case `\i`: + return c.runInclude(cmd[1:], loopState, errState, false /* relative */) + + case `\ir`: + return c.runInclude(cmd[1:], loopState, errState, true /* relative */) + case `\p`: // This is analogous to \show but does not need a special case. // Implemented for compatibility with psql. @@ -1113,6 +1127,65 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu return loopState } +const maxRecursionLevels = 10 + +func (c *cliState) runInclude( + cmd []string, contState, errState cliStateEnum, relative bool, +) (resState cliStateEnum) { + if len(cmd) != 1 { + return c.invalidSyntax(errState, `%s. Try \? for help.`, c.lastInputLine) + } + + if c.levels >= maxRecursionLevels { + c.exitErr = errors.Newf(`\i: too many recursion levels (max %d)`, maxRecursionLevels) + fmt.Fprintf(stderr, "%v\n", c.exitErr) + return errState + } + + if len(c.partialLines) > 0 { + return c.invalidSyntax(errState, `cannot use \i during multi-line entry.`) + } + + filename := cmd[0] + if !filepath.IsAbs(filename) && relative { + // In relative mode, the filename is resolved relative to the + // surrounding script. + filename = filepath.Join(c.includeDir, filename) + } + + f, err := os.Open(filename) + if err != nil { + fmt.Fprintln(stderr, err) + c.exitErr = err + return errState + } + // Close the file at the end. + defer func() { + if err := f.Close(); err != nil { + fmt.Fprintf(stderr, "error: closing %s: %v\n", filename, err) + c.exitErr = errors.CombineErrors(c.exitErr, err) + resState = errState + } + }() + + newState := cliState{ + conn: c.conn, + includeDir: filepath.Dir(filename), + ins: noLineEditor, + buf: bufio.NewReader(f), + levels: c.levels + 1, + } + + if err := newState.doRunShell(cliStartLine, f); err != nil { + // Note: a message was already printed on stderr at the point at + // which the error originated. No need to repeat it here. + c.exitErr = errors.Wrapf(err, "%v", filename) + return errState + } + + return contState +} + func (c *cliState) doPrepareStatementLine( startState, contState, checkState, execState cliStateEnum, ) cliStateEnum { @@ -1293,9 +1366,14 @@ func (c *cliState) doDecidePath() cliStateEnum { // runInteractive runs the SQL client interactively, presenting // a prompt to the user for each statement. func runInteractive(conn *sqlConn, cmdIn *os.File) (exitErr error) { - c := cliState{conn: conn} + c := cliState{ + conn: conn, + includeDir: ".", + } + return c.doRunShell(cliStart, cmdIn) +} - state := cliStart +func (c *cliState) doRunShell(state cliStateEnum, cmdIn *os.File) (exitErr error) { for { if state == cliStop { break diff --git a/pkg/cli/testdata/i_maxrecursion.sql b/pkg/cli/testdata/i_maxrecursion.sql new file mode 100644 index 000000000000..ed682fed945b --- /dev/null +++ b/pkg/cli/testdata/i_maxrecursion.sql @@ -0,0 +1 @@ +\i testdata/i_maxrecursion.sql diff --git a/pkg/cli/testdata/i_multiline.sql b/pkg/cli/testdata/i_multiline.sql new file mode 100644 index 000000000000..4ada06c76eef --- /dev/null +++ b/pkg/cli/testdata/i_multiline.sql @@ -0,0 +1,3 @@ +SELECT -- incomplete statement, \i invalid +\i testdata/i_twolevels2.sql +123; diff --git a/pkg/cli/testdata/i_stopmiddle.sql b/pkg/cli/testdata/i_stopmiddle.sql new file mode 100644 index 000000000000..a578dfa311c7 --- /dev/null +++ b/pkg/cli/testdata/i_stopmiddle.sql @@ -0,0 +1,3 @@ +SELECT 123; +\q +SELECT 456; diff --git a/pkg/cli/testdata/i_twolevels1.sql b/pkg/cli/testdata/i_twolevels1.sql new file mode 100644 index 000000000000..6e513dfcd9b4 --- /dev/null +++ b/pkg/cli/testdata/i_twolevels1.sql @@ -0,0 +1,9 @@ +-- test that \echo spills into the second level +\set echo + +\i testdata/i_twolevels2.sql + +-- at this point, the second level has disabled echo. +-- verify this. +SELECT 456; + diff --git a/pkg/cli/testdata/i_twolevels2.sql b/pkg/cli/testdata/i_twolevels2.sql new file mode 100644 index 000000000000..aa81e22781ba --- /dev/null +++ b/pkg/cli/testdata/i_twolevels2.sql @@ -0,0 +1,7 @@ +SELECT 123; + +-- check the relative include +\ir i_twolevels3.sql + +-- check that disabling echo here spills into the first level. +\unset echo diff --git a/pkg/cli/testdata/i_twolevels3.sql b/pkg/cli/testdata/i_twolevels3.sql new file mode 100644 index 000000000000..b923482e88c9 --- /dev/null +++ b/pkg/cli/testdata/i_twolevels3.sql @@ -0,0 +1 @@ +SELECT 789;