Skip to content

Commit

Permalink
Add support for executing SQL statements from the command line.
Browse files Browse the repository at this point in the history
With this patch the user can use "sql -e ..." to execute statements,
print their results and exit, without an interactive prompt.

Fixes cockroachdb#3817
  • Loading branch information
knz committed Jan 26, 2016
1 parent fedc14b commit b8d2ba8
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 10 deletions.
31 changes: 31 additions & 0 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,37 @@ range_max_bytes: 67108864
// zone ls
}

func Example_sql() {
c := newCLITest()
defer c.Stop()

c.RunWithArgs([]string{"sql", "-e", "create database t; create table t.f (x int, y int); insert into t.f values (42, 69)"})
c.RunWithArgs([]string{"sql", "-e", "select 3", "select * from t.f"})
c.RunWithArgs([]string{"sql", "-e", "begin", "select 3", "commit"})
c.RunWithArgs([]string{"sql", "-e", "select 3; select * from t.f"})

// Output:
// sql -e create database t; create table t.f (x int, y int); insert into t.f values (42, 69)
// OK
// sql -e select 3 select * from t.f
// 1 rows
// 3
// 3
// 1 rows
// x y
// 42 69
// sql -e begin select 3 commit
// OK
// 1 rows
// 3
// 3
// OK
// sql -e select 3; select * from t.f
// 1 rows
// x y
// 42 69
}

func Example_user() {
c := newCLITest()
defer c.Stop()
Expand Down
14 changes: 14 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ var flagUsage = map[string]string{
"password": `
The created user's password. If provided, disables prompting. Pass '-' to provide
the password on standard input.
`,
"execute": `
Execute the SQL statement(s) on the command line, then exit. Each
subsequent positional argument on the command line may contain
one or more SQL statements, separated by semicolons. 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 the last SQL statement in each
positional argument are printed on the standard output.
`,
}

Expand Down Expand Up @@ -250,6 +259,11 @@ func initFlags(ctx *server.Context) {
f.StringVar(&ctx.Certs, "certs", ctx.Certs, flagUsage["certs"])
}

{
f := sqlShellCmd.Flags()
f.BoolVarP(&ctx.OneShotSQL, "execute", "e", ctx.OneShotSQL, flagUsage["execute"])
}

// Max results flag for scan, reverse scan, and range list.
for _, cmd := range []*cobra.Command{scanCmd, reverseScanCmd, lsRangesCmd} {
f := cmd.Flags()
Expand Down
70 changes: 60 additions & 10 deletions cli/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package cli

import (
"database/sql"
"fmt"
"io"
"os"
Expand All @@ -40,17 +41,12 @@ var sqlShellCmd = &cobra.Command{
Long: `
Open a sql shell running against the cockroach database at --addr.
`,
Run: runTerm, // TODO(tschottdorf): should be able to return err code when reading from stdin
Run: runTerm,
}

func runTerm(cmd *cobra.Command, args []string) {
if len(args) != 0 {
mustUsage(cmd)
return
}

db := makeSQLClient()
defer func() { _ = db.Close() }()
// runInteractive runs the SQL client interactively, presenting
// a prompt to the user for each statement.
func runInteractive(db *sql.DB) {

liner := liner.NewLiner()
defer func() {
Expand Down Expand Up @@ -79,6 +75,9 @@ func runTerm(cmd *cobra.Command, args []string) {
var stmt []string
var l string
var err error

exitCode := 0

for {
if len(stmt) == 0 {
l, err = liner.Prompt(fullPrompt)
Expand All @@ -88,6 +87,7 @@ func runTerm(cmd *cobra.Command, args []string) {
if err != nil {
if err != io.EOF {
fmt.Fprintf(osStderr, "Input error: %s\n", err)
exitCode = 1
}
break
}
Expand All @@ -112,11 +112,61 @@ func runTerm(cmd *cobra.Command, args []string) {
fullStmt := strings.Join(stmt, "\n")
liner.AppendHistory(fullStmt)

exitCode = 0
if err := runPrettyQuery(db, os.Stdout, fullStmt); err != nil {
fmt.Println(err)
fmt.Fprintln(osStderr, err)
exitCode = 1
}

// Clear the saved statement.
stmt = stmt[:0]
}

if exitCode != 0 {
os.Exit(exitCode)
}
}

// runOneStatement executes one statement and terminates
// on error.
func runStatements(db *sql.DB, stmts []string) {
for _, stmt := range stmts {
fullStmt := stmt + "\n"
cols, allRows, err := runQuery(db, fullStmt)
if err != nil {
fmt.Fprintln(osStderr, err)
os.Exit(1)
}

if len(cols) == 0 {
// no result select, inform user
fmt.Fprintln(os.Stdout, "OK")
} else {
// some results, inform user about how much data to expect
fmt.Fprintln(os.Stdout, len(allRows), "rows")
fmt.Fprintln(os.Stdout, strings.Join(cols, "\t"))
for _, row := range allRows {
fmt.Fprintln(os.Stdout, strings.Join(row, "\t"))
}
}

}
}

func runTerm(cmd *cobra.Command, args []string) {
if !(len(args) >= 1 && context.OneShotSQL) && len(args) != 0 {
mustUsage(cmd)
return
}

db := makeSQLClient()
defer func() { _ = db.Close() }()

if context.OneShotSQL {
// single-line sql; run as simple as possible, without noise on stdout.
runStatements(db, args)
} else {
runInteractive(db)
}

}
6 changes: 6 additions & 0 deletions server/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ type Context struct {
// TimeUntilStoreDead is the time after which if there is no new gossiped
// information about a store, it is considered dead.
TimeUntilStoreDead time.Duration

// OneShotSQL indicates the SQL client should run the command-line
// statement(s) and terminate directly, without presenting a REPL to
// the user.
OneShotSQL bool
}

// NewContext returns a Context with default values.
Expand All @@ -155,6 +160,7 @@ func (ctx *Context) InitDefaults() {
ctx.MetricsFrequency = defaultMetricsFrequency
ctx.TimeUntilStoreDead = defaultTimeUntilStoreDead
ctx.BalanceMode = defaultBalanceMode
ctx.OneShotSQL = false
}

// Get the stores on both start and init.
Expand Down

0 comments on commit b8d2ba8

Please sign in to comment.