Skip to content

Commit

Permalink
cli: conditionally output unicode characters in the output of sql -e
Browse files Browse the repository at this point in the history
Summary:

- by default, `sql -e` always escapes strings that contain special
  characters. It also forcefully escapes strings containing double
  quotes and backslashes, so as to ease automated processing by other
  languages (ie. a string starting with a doublequote character in the
  output will always be the start of an escaped string.). This
  way issue #4315 is still addressed adequately.

- a new flag `--pretty` is added to the `sql` sub-command, to print
  row data as pretty-formatted ASCII art tables.

- when results are pretty-formatted (either in the interactive shell
  or with `sql --pretty -e`), graphic unicode and newline characters
  are printed without escaping. This keeps the fix to #6926.

- new tests are added in `cli_test.go` (`Example_sql_escape`) to test
  escaped and non-escaped output when table and column names contain
  special characters.

- the `cli` API renames `runPrettyQuery` to
  `runQueryAndFormatResults`, and extends it with an extra argument to
  specify pretty-printing.

(See issues/PRs #7045, #6926, #4354 and #4315 for background discussion.)
  • Loading branch information
knz committed Jun 6, 2016
1 parent 8a08cd9 commit fdae424
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 108 deletions.
87 changes: 79 additions & 8 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,20 +611,34 @@ func Example_sql_escape() {

c.RunWithArgs([]string{"sql", "-e", "create database t; create table t.t (s string, d string);"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'foo', 'printable ASCII')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'foo\\x0a', 'non-printable ASCII')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\"foo', 'printable ASCII with quotes')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\\\\foo', 'printable ASCII with backslash')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'foo\\x0abar', 'non-printable ASCII')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values ('κόσμε', 'printable UTF8')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\\xc3\\xb1', 'printable UTF8 using escapes')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\\x01', 'non-printable UTF8 string')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\\xdc\\x88\\x38\\x35', 'UTF8 string with RTL char')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'\\xc3\\x28', 'non-UTF8 string')"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.t values (e'a\\tb\\tc\\n12\\t123123213\\t12313', 'tabs')"})
c.RunWithArgs([]string{"sql", "-e", "select * from t.t"})
c.RunWithArgs([]string{"sql", "-e", "create table t.u (\"\"\"foo\" int, \"\\foo\" int, \"foo\nbar\" int, \"κόσμε\" int, \"܈85\" int)"})
c.RunWithArgs([]string{"sql", "-e", "insert into t.u values (0, 0, 0, 0, 0)"})
c.RunWithArgs([]string{"sql", "-e", "show columns from t.u"})
c.RunWithArgs([]string{"sql", "-e", "select * from t.u"})
c.RunWithArgs([]string{"sql", "--pretty", "-e", "select * from t.t"})
c.RunWithArgs([]string{"sql", "--pretty", "-e", "show columns from t.u"})
c.RunWithArgs([]string{"sql", "--pretty", "-e", "select * from t.u"})

// Output:
// sql -e create database t; create table t.t (s string, d string);
// CREATE TABLE
// sql -e insert into t.t values (e'foo', 'printable ASCII')
// INSERT 1
// sql -e insert into t.t values (e'foo\x0a', 'non-printable ASCII')
// sql -e insert into t.t values (e'"foo', 'printable ASCII with quotes')
// INSERT 1
// sql -e insert into t.t values (e'\\foo', 'printable ASCII with backslash')
// INSERT 1
// sql -e insert into t.t values (e'foo\x0abar', 'non-printable ASCII')
// INSERT 1
// sql -e insert into t.t values ('κόσμε', 'printable UTF8')
// INSERT 1
Expand All @@ -636,17 +650,74 @@ func Example_sql_escape() {
// INSERT 1
// sql -e insert into t.t values (e'\xc3\x28', 'non-UTF8 string')
// INSERT 1
// sql -e insert into t.t values (e'a\tb\tc\n12\t123123213\t12313', 'tabs')
// INSERT 1
// sql -e select * from t.t
// 7 rows
// 10 rows
// s d
// foo printable ASCII
// foo
// non-printable ASCII
// κόσμε printable UTF8
// ñ printable UTF8 using escapes
// "\"foo" printable ASCII with quotes
// "\\foo" printable ASCII with backslash
// "foo\nbar" non-printable ASCII
// "\u03ba\u1f79\u03c3\u03bc\u03b5" printable UTF8
// "\u00f1" printable UTF8 using escapes
// "\x01" non-printable UTF8 string
// ܈85 UTF8 string with RTL char
// "\u070885" UTF8 string with RTL char
// "\xc3(" non-UTF8 string
// "a\tb\tc\n12\t123123213\t12313" tabs
// sql -e create table t.u ("""foo" int, "\foo" int, "foo
// bar" int, "κόσμε" int, "܈85" int)
// CREATE TABLE
// sql -e insert into t.u values (0, 0, 0, 0, 0)
// INSERT 1
// sql -e show columns from t.u
// 6 rows
// Field Type Null Default
// "\"foo" INT true NULL
// "\\foo" INT true NULL
// "foo\nbar" INT true NULL
// "\u03ba\u1f79\u03c3\u03bc\u03b5" INT true NULL
// "\u070885" INT true NULL
// rowid INT false unique_rowid()
// sql -e select * from t.u
// 1 row
// "\"foo" "\\foo" "foo\nbar" "\u03ba\u1f79\u03c3\u03bc\u03b5" "\u070885"
// 0 0 0 0 0
// sql --pretty -e select * from t.t
// +--------------------------------+--------------------------------+
// | s | d |
// +--------------------------------+--------------------------------+
// | foo | printable ASCII |
// | "foo | printable ASCII with quotes |
// | \foo | printable ASCII with backslash |
// | foo␤ | non-printable ASCII |
// | bar | |
// | κόσμε | printable UTF8 |
// | ñ | printable UTF8 using escapes |
// | "\x01" | non-printable UTF8 string |
// | ܈85 | UTF8 string with RTL char |
// | "\xc3(" | non-UTF8 string |
// | a b c␤ | tabs |
// | 12 123123213 12313 | |
// +--------------------------------+--------------------------------+
// sql --pretty -e show columns from t.u
// +----------+------+-------+----------------+
// | Field | Type | Null | Default |
// +----------+------+-------+----------------+
// | "foo | INT | true | NULL |
// | \foo | INT | true | NULL |
// | foo␤ | INT | true | NULL |
// | bar | | | |
// | κόσμε | INT | true | NULL |
// | ܈85 | INT | true | NULL |
// | rowid | INT | false | unique_rowid() |
// +----------+------+-------+----------------+
// sql --pretty -e select * from t.u
// +------+------+------------+-------+-----+
// | "foo | \foo | "foo\nbar" | κόσμε | ܈85 |
// +------+------+------------+-------+-----+
// | 0 | 0 | 0 | 0 | 0 |
// +------+------+------------+-------+-----+
}

func Example_user() {
Expand Down
1 change: 1 addition & 0 deletions cli/cliflags/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
DatabaseName = "database"
DepsName = "deps"
ExecuteName = "execute"
PrettyName = "pretty"
JoinName = "join"
HostName = "host"
InsecureName = "insecure"
Expand Down
4 changes: 4 additions & 0 deletions cli/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type sqlContext struct {

// execStmts is a list of statements to execute.
execStmts statementsValue

// prettyFmt indicates whether tables should be pretty-formatted in
// the output during non-interactive execution.
prettyFmt bool
}

type debugContext struct {
Expand Down
5 changes: 5 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ 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.`),

cliflags.PrettyName: wrapText(`
Causes table rows to be formatted as tables using ASCII art.
When not specified, table rows are printed as tab-separated values (TSV).`),

cliflags.JoinName: wrapText(`
A comma-separated list of addresses to use when a new node is joining
an existing cluster. For the first node in a cluster, --join should
Expand Down Expand Up @@ -441,6 +445,7 @@ func init() {
{
f := sqlShellCmd.Flags()
f.VarP(&sqlCtx.execStmts, cliflags.ExecuteName, "e", usageNoEnv(cliflags.ExecuteName))
f.BoolVar(&sqlCtx.prettyFmt, cliflags.PrettyName, false, usageNoEnv(cliflags.PrettyName))
}
{
f := freezeClusterCmd.PersistentFlags()
Expand Down
4 changes: 2 additions & 2 deletions cli/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func runLsNodes(cmd *cobra.Command, args []string) error {
})
}

printQueryOutput(os.Stdout, lsNodesColumnHeaders, rows, "")
printQueryOutput(os.Stdout, lsNodesColumnHeaders, rows, "", true)
return nil
}

Expand Down Expand Up @@ -141,7 +141,7 @@ func runStatusNode(cmd *cobra.Command, args []string) error {
return util.Errorf("expected no arguments or a single node ID")
}

printQueryOutput(os.Stdout, nodesColumnHeaders, nodeStatusesToRows(nodeStatuses), "")
printQueryOutput(os.Stdout, nodesColumnHeaders, nodeStatusesToRows(nodeStatuses), "", true)
return nil
}

Expand Down
36 changes: 5 additions & 31 deletions cli/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@ import (
"strings"

"github.com/chzyer/readline"
"github.com/cockroachdb/cockroach/util"
"github.com/cockroachdb/cockroach/util/envutil"
"github.com/cockroachdb/cockroach/util/log"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"

"github.com/cockroachdb/pq"
)

const (
Expand Down Expand Up @@ -300,7 +297,7 @@ func runInteractive(conn *sqlConn) (exitErr error) {
addHistory(strings.Join(stmt, " "))
}

if exitErr = runPrettyQuery(conn, os.Stdout, makeQuery(fullStmt)); exitErr != nil {
if exitErr = runQueryAndFormatResults(conn, os.Stdout, makeQuery(fullStmt), true); exitErr != nil {
fmt.Fprintln(osStderr, exitErr)
}

Expand All @@ -313,33 +310,10 @@ func runInteractive(conn *sqlConn) (exitErr error) {

// runOneStatement executes one statement and terminates
// on error.
func runStatements(conn *sqlConn, stmts []string) error {
func runStatements(conn *sqlConn, stmts []string, pretty bool) error {
for _, stmt := range stmts {
q := makeQuery(stmt)
for {
cols, allRows, tag, err := runQuery(conn, q)
if err != nil {
if err == pq.ErrNoMoreResults {
break
}
return err
}

if len(cols) == 0 {
// No result selected, inform the user.
fmt.Fprintln(os.Stdout, tag)
} else {
// Some results selected, inform the user about how much data to expect.
fmt.Fprintf(os.Stdout, "%d row%s\n", len(allRows),
util.Pluralize(int64(len(allRows))))

// Then print the results themselves.
fmt.Fprintln(os.Stdout, strings.Join(cols, "\t"))
for _, row := range allRows {
fmt.Fprintln(os.Stdout, strings.Join(row, "\t"))
}
}
q = nextResult
if err := runQueryAndFormatResults(conn, os.Stdout, makeQuery(stmt), pretty); err != nil {
return err
}
}
return nil
Expand All @@ -359,7 +333,7 @@ func runTerm(cmd *cobra.Command, args []string) error {

if len(sqlCtx.execStmts) > 0 {
// Single-line sql; run as simple as possible, without noise on stdout.
return runStatements(conn, sqlCtx.execStmts)
return runStatements(conn, sqlCtx.execStmts, sqlCtx.prettyFmt)
}
return runInteractive(conn)
}
Loading

0 comments on commit fdae424

Please sign in to comment.