Skip to content

Commit

Permalink
clisqlshell: new infrastructure for describe commands
Browse files Browse the repository at this point in the history
Release note: None
  • Loading branch information
knz committed Sep 16, 2022
1 parent f21961c commit ebf5ce9
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 87 deletions.
2 changes: 2 additions & 0 deletions pkg/cli/clisqlshell/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go_library(
srcs = [
"api.go",
"context.go",
"describe.go",
"doc.go",
"parser.go",
"sql.go",
Expand Down Expand Up @@ -40,6 +41,7 @@ go_test(
name = "clisqlshell_test",
srcs = [
"main_test.go",
"describe_test.go",
"sql_internal_test.go",
"sql_test.go",
],
Expand Down
58 changes: 58 additions & 0 deletions pkg/cli/clisqlshell/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package clisqlshell

import (
"strings"

"github.com/cockroachdb/errors"
)

type dkey struct {
prefix string
nargs int
}

var dcmds = map[dkey]func(bool, bool) string{
{`l`, 0}: func(p, s bool) string { return `SHOW DATABASES` },
{`d`, 0}: func(p, s bool) string { return `SHOW TABLES` },
{`d`, 1}: func(p, s bool) string { return `SHOW COLUMNS FROM $1` },
{`dT`, 0}: func(p, s bool) string { return `SHOW TYPES` },
{`dt`, 0}: func(p, s bool) string { return `SHOW TABLES` },
{`du`, 0}: func(p, s bool) string { return `SHOW USERS` },
{`du`, 1}: func(p, s bool) string { return `SELECT * FROM [SHOW USERS] WHERE username = $1` },
{`dd`, 1}: func(p, s bool) string { return `SHOW CONSTRAINTS FROM $1 WITH COMMENT` },
}

func (c *cliState) pgInspect(args []string) (sql string, qargs []interface{}, err error) {
origCmd := args[0]
// Strip the leading `\`.
cmd := origCmd[1:]
args = args[1:]

plus := strings.Contains(cmd, "+")
inclSystem := strings.Contains(cmd, "S")
// Remove the characters "S" and "+" from the describe command.
cmd = strings.TrimRight(cmd, "S+")

key := dkey{cmd, len(args)}
fn := dcmds[key]
if fn == nil {
return "", nil, errors.WithHint(
errors.Newf("unsupported command: %s with %d arguments", origCmd, len(args)),
"Use the SQL SHOW statement to inspect your schema.")
}

for _, a := range args {
qargs = append(qargs, a)
}
return fn(plus, inclSystem), qargs, nil
}
68 changes: 68 additions & 0 deletions pkg/cli/clisqlshell/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package clisqlshell_test

import "github.com/cockroachdb/cockroach/pkg/cli"

func Example_describe_unknown() {
c := cli.NewCLITest(cli.TestCLIParams{})
defer c.Cleanup()

c.RunWithArgs([]string{`sql`, `-e`, `\set echo`, `-e`, `\dz`})

// Output:
// sql -e \set echo -e \dz
// ERROR: unsupported command: \dz with 0 arguments
// HINT: Use the SQL SHOW statement to inspect your schema.
// ERROR: -e: unsupported command: \dz with 0 arguments
// HINT: Use the SQL SHOW statement to inspect your schema.
}

var describeCmdPrefix = []string{
`sql`, `-e`, `\set display_format csv`, `-e`, `\set echo`, `-e`,
}

func Example_describe_l() {
c := cli.NewCLITest(cli.TestCLIParams{})
defer c.Cleanup()

c.RunWithArgs([]string{`sql`, `-e`, `create database mydb`})
c.RunWithArgs(append(describeCmdPrefix, `\l`))

// Output:
// sql -e create database mydb
// CREATE DATABASE
// sql -e \set display_format csv -e \set echo -e \l
// > SHOW DATABASES
// database_name,owner,primary_region,secondary_region,regions,survival_goal
// defaultdb,root,NULL,NULL,{},NULL
// mydb,root,NULL,NULL,{},NULL
// postgres,root,NULL,NULL,{},NULL
// system,node,NULL,NULL,{},NULL
}

func Example_describe_du() {
c := cli.NewCLITest(cli.TestCLIParams{})
defer c.Cleanup()

c.RunWithArgs([]string{`sql`, `-e`, `CREATE USER my_user WITH CREATEDB; GRANT admin TO my_user;`})
c.RunWithArgs(append(describeCmdPrefix, `\du`))
// Output:
// sql -e CREATE USER my_user WITH CREATEDB; GRANT admin TO my_user;
// CREATE ROLE
// GRANT
// sql -e \set display_format csv -e \set echo -e \du
// > SHOW USERS
// username,options,member_of
// admin,,{}
// my_user,CREATEDB,{admin}
// root,,{admin}
}
80 changes: 39 additions & 41 deletions pkg/cli/clisqlshell/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,34 @@ func (c *cliState) setupChangefeedOutput() (undo func(), err error) {

}

func (c *cliState) handleDescribe(cmd []string, loopState, errState cliStateEnum) cliStateEnum {
var sql string
var qargs []interface{}
sql, qargs, c.exitErr = c.pgInspect(cmd)
if c.exitErr != nil {
clierror.OutputError(c.iCtx.stderr, c.exitErr, true /*showSeverity*/, false /*verbose*/)
return errState
}
c.exitErr = c.runWithInterruptableCtx(func(ctx context.Context) error {
q := clisqlclient.MakeQuery(sql, qargs...)
return c.sqlExecCtx.RunQueryAndFormatResults(
ctx,
c.conn,
c.iCtx.queryOutput, // query output.
c.iCtx.stdout, // timings.
c.iCtx.stderr,
q,
)
})
if c.exitErr != nil {
if !c.singleStatement {
clierror.OutputError(c.iCtx.stderr, c.exitErr, true /*showSeverity*/, false /*verbose*/)
}
return errState
}
return loopState
}

func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnum {
if len(c.lastInputLine) == 0 || c.lastInputLine[0] != '\\' {
return nextState
Expand All @@ -1207,6 +1235,17 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu
line := strings.TrimRight(c.lastInputLine, "; ")

cmd := strings.Fields(line)
if cmd[0] == `\z` {
// psql compatibility.
cmd[0] = `\dp`
}
if cmd[0] == `\sf` || cmd[0] == `\sf+` ||
cmd[0] == `\sv` || cmd[0] == `\sv+` ||
cmd[0] == `\l` || cmd[0] == `\l+` ||
(strings.HasPrefix(cmd[0], `\d`) && cmd[0] != `\demo`) {
return c.handleDescribe(cmd, loopState, errState)
}

switch cmd[0] {
case `\q`, `\quit`, `\exit`:
return cliStop
Expand Down Expand Up @@ -1275,14 +1314,6 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu
}
return c.handleFunctionHelp(cmd[1:], loopState, errState)

case `\l`:
c.concatLines = `SHOW DATABASES`
return cliRunStatement

case `\dt`:
c.concatLines = `SHOW TABLES`
return cliRunStatement

case `\copy`:
c.exitErr = c.runWithInterruptableCtx(func(ctx context.Context) error {
// Strip out the starting \ in \copy.
Expand All @@ -1305,35 +1336,6 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu
}
return c.invalidSyntax(errState)

case `\dT`:
c.concatLines = `SHOW TYPES`
return cliRunStatement

case `\du`:
if len(cmd) == 1 {
c.concatLines = `SHOW USERS`
return cliRunStatement
} else if len(cmd) == 2 {
c.concatLines = fmt.Sprintf(`SELECT * FROM [SHOW USERS] WHERE username = %s`, lexbase.EscapeSQLString(cmd[1]))
return cliRunStatement
}
return c.invalidSyntax(errState)

case `\d`:
if len(cmd) == 1 {
c.concatLines = `SHOW TABLES`
return cliRunStatement
} else if len(cmd) == 2 {
c.concatLines = `SHOW COLUMNS FROM ` + cmd[1]
return cliRunStatement
}
return c.invalidSyntax(errState)
case `\dd`:
if len(cmd) == 2 {
c.concatLines = `SHOW CONSTRAINTS FROM ` + cmd[1] + ` WITH COMMENT`
return cliRunStatement
}
return c.invalidSyntax(errState)
case `\connect`, `\c`:
return c.handleConnect(cmd[1:], loopState, errState)

Expand Down Expand Up @@ -1366,10 +1368,6 @@ func (c *cliState) doHandleCliCmd(loopState, nextState cliStateEnum) cliStateEnu
return c.handleStatementDiag(cmd[1:], loopState, errState)

default:
if strings.HasPrefix(cmd[0], `\d`) {
// Unrecognized command for now, but we want to be helpful.
fmt.Fprint(c.iCtx.stderr, "Suggestion: use the SQL SHOW statement to inspect your schema.\n")
}
return c.invalidSyntax(errState)
}

Expand Down
32 changes: 1 addition & 31 deletions pkg/cli/clisqlshell/sql_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,48 +89,18 @@ func TestIsEndOfStatement(t *testing.T) {
}
}

// Test handleCliCmd cases for client-side commands that are aliases for sql
// statements.
func TestHandleCliCmdSqlAlias(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

clientSideCommandTestsTable := []struct {
commandString string
wantSQLStmt string
}{
{`\l`, `SHOW DATABASES`},
{`\dt`, `SHOW TABLES`},
{`\dT`, `SHOW TYPES`},
{`\du`, `SHOW USERS`},
{`\du myuser`, `SELECT * FROM [SHOW USERS] WHERE username = 'myuser'`},
{`\d mytable`, `SHOW COLUMNS FROM mytable`},
{`\d`, `SHOW TABLES`},
}

for _, tt := range clientSideCommandTestsTable {
c := setupTestCliState()
c.lastInputLine = tt.commandString
gotState := c.doHandleCliCmd(cliStateEnum(0), cliStateEnum(1))

assert.Equal(t, cliRunStatement, gotState)
assert.Equal(t, tt.wantSQLStmt, c.concatLines)
}
}

func TestHandleCliCmdSlashDInvalidSyntax(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

clientSideCommandTests := []string{`\d goodarg badarg`, `\dz`}
clientSideCommandTests := []string{`\d goodarg badarg`}

for _, tt := range clientSideCommandTests {
c := setupTestCliState()
c.lastInputLine = tt
gotState := c.doHandleCliCmd(cliStateEnum(0), cliStateEnum(1))

assert.Equal(t, cliStateEnum(0), gotState)
assert.Equal(t, errInvalidSyntax, c.exitErr)
}
}

Expand Down
17 changes: 2 additions & 15 deletions pkg/cli/clisqlshell/sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Example_sql() {
c.RunWithArgs([]string{`sql`, `-e`, `begin`, `-e`, `select 3 as "3"`, `-e`, `commit`})
c.RunWithArgs([]string{`sql`, `-e`, `select * from t.f`})
c.RunWithArgs([]string{`sql`, `--execute=SELECT database_name, owner FROM [show databases]`})
c.RunWithArgs([]string{`sql`, `-e`, `\l`, `-e`, `\echo hello`})
c.RunWithArgs([]string{`sql`, `-e`, `\echo hello`})
c.RunWithArgs([]string{`sql`, `-e`, `select 1 as "1"; select 2 as "2"`})
c.RunWithArgs([]string{`sql`, `-e`, `select 1 as "1"; select 2 as "@" where false`})
// CREATE TABLE AS returns a SELECT tag with a row count, check this.
Expand All @@ -59,8 +59,6 @@ func Example_sql() {
// first batch consisting of 1 row has been returned to the client.
c.RunWithArgs([]string{`sql`, `-e`, `select 1/(@1-2) from generate_series(1,3)`})
c.RunWithArgs([]string{`sql`, `-e`, `SELECT '20:01:02+03:04:05'::timetz AS regression_65066`})
c.RunWithArgs([]string{`sql`, `-e`, `CREATE USER my_user WITH CREATEDB; GRANT admin TO my_user;`})
c.RunWithArgs([]string{`sql`, `-e`, `\du my_user`})

// Output:
// sql -e show application_name
Expand Down Expand Up @@ -89,12 +87,7 @@ func Example_sql() {
// postgres root
// system node
// t root
// sql -e \l -e \echo hello
// database_name owner primary_region secondary_region regions survival_goal
// defaultdb root NULL NULL {} NULL
// postgres root NULL NULL {} NULL
// system node NULL NULL {} NULL
// t root NULL NULL {} NULL
// sql -e \echo hello
// hello
// sql -e select 1 as "1"; select 2 as "2"
// 1
Expand Down Expand Up @@ -125,12 +118,6 @@ func Example_sql() {
// sql -e SELECT '20:01:02+03:04:05'::timetz AS regression_65066
// regression_65066
// 20:01:02+03:04:05
// sql -e CREATE USER my_user WITH CREATEDB; GRANT admin TO my_user;
// CREATE ROLE
// GRANT
// sql -e \du my_user
// username options member_of
// my_user CREATEDB {admin}
}

func Example_sql_config() {
Expand Down

0 comments on commit ebf5ce9

Please sign in to comment.