From 7a0f3d61f0e8534d284a0e35c3f2e618c00d30cd Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Thu, 24 Sep 2020 12:57:01 +0200 Subject: [PATCH] cli: support `-f` to read SQL from a file Release note (cli change): `cockroach sql` and `cockroach demo` now support the command-line parameter `--input-file` (shorthand `-f`) to read commands from a named file. The behavior is the same as if the file was redirected on the standard input; in particular, the processing stops at the first error encountered (which is different from interactive usage with a prompt). Note that it is not (yet) possible to combine `-f` with `-e`. --- pkg/cli/cli_test.go | 29 +++++++++++++++++++++++++++++ pkg/cli/cliflags/flags.go | 15 ++++++++++++++- pkg/cli/context.go | 7 +++++++ pkg/cli/demo.go | 11 ++++++++--- pkg/cli/flags.go | 1 + pkg/cli/sql.go | 31 +++++++++++++++++++++++++++---- pkg/cli/testdata/inputfile.sql | 18 ++++++++++++++++++ 7 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 pkg/cli/testdata/inputfile.sql diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index c672b17c0340..f6f878e71c73 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -2060,3 +2060,32 @@ func Example_dump_no_visible_columns() { // CREATE TABLE public.t (FAMILY "primary" (rowid) // ); } + +// Example_read_from_file tests the -f parameter. +// The input file contains a mix of client-side and +// server-side commands to ensure that both are supported with -f. +func Example_read_from_file() { + c := newCLITest(cliTestParams{}) + defer c.cleanup() + + c.RunWithArgs([]string{"sql", "-e", "select 1", "-f", "testdata/inputfile.sql"}) + c.RunWithArgs([]string{"sql", "-f", "testdata/inputfile.sql"}) + + // Output: + // sql -e select 1 -f testdata/inputfile.sql + // ERROR: unsupported combination: --execute and --file + // sql -f testdata/inputfile.sql + // SET + // CREATE TABLE + // > INSERT INTO test(s) VALUES ('hello'), ('world'); + // INSERT 2 + // > SELECT * FROM test; + // s + // hello + // world + // > SELECT undefined; + // ERROR: column "undefined" does not exist + // SQLSTATE: 42703 + // ERROR: column "undefined" does not exist + // SQLSTATE: 42703 +} diff --git a/pkg/cli/cliflags/flags.go b/pkg/cli/cliflags/flags.go index 41d31cd75b5a..a759fc99b299 100644 --- a/pkg/cli/cliflags/flags.go +++ b/pkg/cli/cliflags/flags.go @@ -256,7 +256,20 @@ Execute the SQL statement(s) on the command line, then exit. This flag may be specified multiple times and each value may contain multiple semicolon separated statements. If an error occurs in any statement, the command exits with a non-zero status code and further statements are not executed. The -results of each SQL statement are printed on the standard output.`, +results of each SQL statement are printed on the standard output. + +This flag is incompatible with --file / -f.`, + } + + File = FlagInfo{ + Name: "file", + Shorthand: "f", + Description: ` +Read and execute the SQL statement(s) from the specified file. +The file is processed as if it has been redirected on the standard +input of the shell. + +This flag is incompatible with --execute / -e.`, } Watch = FlagInfo{ diff --git a/pkg/cli/context.go b/pkg/cli/context.go index 1a73a142a41f..554dea0fd0c3 100644 --- a/pkg/cli/context.go +++ b/pkg/cli/context.go @@ -204,8 +204,14 @@ var sqlCtx = struct { setStmts statementsValue // execStmts is a list of statements to execute. + // Only valid if inputFile is empty. execStmts statementsValue + // inputFile is the file to read from. + // If empty, os.Stdin is used. + // Only valid if execStmts is empty. + inputFile string + // repeatDelay indicates that the execStmts should be "watched" // at the specified time interval. Zero disables // the watch. @@ -240,6 +246,7 @@ var sqlCtx = struct { func setSQLContextDefaults() { sqlCtx.setStmts = nil sqlCtx.execStmts = nil + sqlCtx.inputFile = "" sqlCtx.repeatDelay = 0 sqlCtx.safeUpdates = false sqlCtx.showTimes = false diff --git a/pkg/cli/demo.go b/pkg/cli/demo.go index a11c295c3735..8ab9ffe2704f 100644 --- a/pkg/cli/demo.go +++ b/pkg/cli/demo.go @@ -14,7 +14,6 @@ import ( "context" gosql "database/sql" "fmt" - "os" "strings" "time" @@ -253,6 +252,12 @@ func checkDemoConfiguration( } func runDemo(cmd *cobra.Command, gen workload.Generator) (err error) { + cmdIn, closeFn, err := getInputFile() + if err != nil { + return err + } + defer closeFn() + if gen, err = checkDemoConfiguration(cmd, gen); err != nil { return err } @@ -283,7 +288,7 @@ func runDemo(cmd *cobra.Command, gen workload.Generator) (err error) { } demoCtx.transientCluster = &c - checkInteractive(os.Stdin) + checkInteractive(cmdIn) if cliCtx.isInteractive { fmt.Printf(`# @@ -359,7 +364,7 @@ func runDemo(cmd *cobra.Command, gen workload.Generator) (err error) { conn := makeSQLConn(c.connURL) defer conn.Close() - return runClient(cmd, conn) + return runClient(cmd, conn, cmdIn) } func waitForLicense(licenseDone <-chan error) error { diff --git a/pkg/cli/flags.go b/pkg/cli/flags.go index 17e816866291..47bb2666089f 100644 --- a/pkg/cli/flags.go +++ b/pkg/cli/flags.go @@ -656,6 +656,7 @@ func init() { f := cmd.Flags() varFlag(f, &sqlCtx.setStmts, cliflags.Set) varFlag(f, &sqlCtx.execStmts, cliflags.Execute) + stringFlag(f, &sqlCtx.inputFile, cliflags.File) durationFlag(f, &sqlCtx.repeatDelay, cliflags.Watch) boolFlag(f, &sqlCtx.safeUpdates, cliflags.SafeUpdates) boolFlag(f, &sqlCtx.debugMode, cliflags.CliDebugMode) diff --git a/pkg/cli/sql.go b/pkg/cli/sql.go index f0aa7625c742..3840ab7b5aea 100644 --- a/pkg/cli/sql.go +++ b/pkg/cli/sql.go @@ -1486,8 +1486,31 @@ func checkInteractive(stdin *os.File) { cliCtx.isInteractive = len(sqlCtx.execStmts) == 0 && isatty.IsTerminal(stdin.Fd()) } +// getInputFile establishes where we are reading from. +func getInputFile() (cmdIn *os.File, closeFn func(), err error) { + if sqlCtx.inputFile == "" { + return os.Stdin, func() {}, nil + } + + if len(sqlCtx.execStmts) != 0 { + return nil, nil, errors.Newf("unsupported combination: --%s and --%s", cliflags.Execute.Name, cliflags.File.Name) + } + + f, err := os.Open(sqlCtx.inputFile) + if err != nil { + return nil, nil, err + } + return f, func() { _ = f.Close() }, nil +} + func runTerm(cmd *cobra.Command, args []string) error { - checkInteractive(os.Stdin) + cmdIn, closeFn, err := getInputFile() + if err != nil { + return err + } + defer closeFn() + + checkInteractive(cmdIn) if cliCtx.isInteractive { // The user only gets to see the welcome message on interactive sessions. @@ -1500,10 +1523,10 @@ func runTerm(cmd *cobra.Command, args []string) error { } defer conn.Close() - return runClient(cmd, conn) + return runClient(cmd, conn, cmdIn) } -func runClient(cmd *cobra.Command, conn *sqlConn) error { +func runClient(cmd *cobra.Command, conn *sqlConn, cmdIn *os.File) error { // Open the connection to make sure everything is OK before running any // statements. Performs authentication. if err := conn.ensureConn(); err != nil { @@ -1513,7 +1536,7 @@ func runClient(cmd *cobra.Command, conn *sqlConn) error { // Enable safe updates, unless disabled. setupSafeUpdates(cmd, conn) - return runInteractive(conn, os.Stdin) + return runInteractive(conn, cmdIn) } // setupSafeUpdates attempts to enable "safe mode" if the session is diff --git a/pkg/cli/testdata/inputfile.sql b/pkg/cli/testdata/inputfile.sql new file mode 100644 index 000000000000..c1d5c46fadcd --- /dev/null +++ b/pkg/cli/testdata/inputfile.sql @@ -0,0 +1,18 @@ +--- input file for Example_read_from_file. + +--- don't report timestamps: it makes the output non-deterministic. +\unset show_times + +USE defaultdb; +CREATE TABLE test(s STRING); + +-- make the reminder echo its SQL. +\set echo +INSERT INTO test(s) VALUES ('hello'), ('world'); +SELECT * FROM test; + +-- produce an error, to test that processing stops. +SELECT undefined; + +-- this is not executed +SELECT 'unseen';