Minor cleanup to some fbsql flags and meta-commands (#2299)

* Set fbsql prompt based on the connected database

This also changes the prompt to align with psql, where it begins with:
db=#
and the mid looks like:
db-#

* Require organizationID in on-prem, serverless queries

* Support meta-commands in `--file` command

I'm not sure why this was restricted before. Just an oversight.

* Change default history file name to fbsql_history

* Add port short flag: p

* Support meta command \list (for \l) and \out (for \o)

* cleanup while writing docs

* Have \cd with no arguments change to home directory

* Avoid shadowing `action`
This commit is contained in:
Travis Turner 2023-03-07 12:18:55 -06:00 • committed by GitHub
parent 0708673df5
commit 05ebdd15f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 59 deletions

View file

@ -21,8 +21,6 @@ import (
const (
defaultHost string = "localhost"
promptBegin string = "fbsql> "
promptMid string = " -> "
terminationChar string = ";"
nullValue string = "NULL"
)
@ -204,7 +202,7 @@ func (cmd *Command) run(ctx context.Context) error {
cmd.setupHistory()
rl, err := readline.NewEx(&readline.Config{
Prompt: promptBegin,
Prompt: cmd.prompt(false),
HistoryFile: cmd.historyPath,
HistoryLimit: 100000,
DisableAutoSaveHistory: true,
@ -223,11 +221,7 @@ func (cmd *Command) run(ctx context.Context) error {
var inMidCommand bool
for {
if inMidCommand {
rl.SetPrompt(promptMid)
} else {
rl.SetPrompt(promptBegin)
}
rl.SetPrompt(cmd.prompt(inMidCommand))
// Read user provided input.
line, err := rl.Readline()
@ -331,6 +325,20 @@ func (cmd *Command) run(ctx context.Context) error {
}
}
// prompt constructs the prompt that the user sees based on the currently
// connected database and whether the user is in the middle of a sql statement.
func (cmd *Command) prompt(mid bool) string {
db := "fbsql" // default prompt when a database is not set.
if cmd.databaseName != "" {
db = cmd.databaseName
}
if mid {
return strings.Repeat(" ", len(db)) + "-# "
}
return db + "=# "
}
// close is called upon quitting. It should close any remaining open file
// handles used by the CLICommand.
func (cmd *Command) close() error {
@ -358,6 +366,12 @@ func (cmd *Command) setupConfig() error {
func (cmd *Command) executeAndWriteQuery(qry query) error {
queryResponse, err := cmd.executeQuery(qry)
if err != nil {
if errors.Is(err, ErrOrganizationRequired) {
// Print an error message and return nil, effectively aborting any
// further writes for this query.
cmd.Errorf("Organization required. Use \\org to set an organization.\n")
return nil
}
return errors.Wrap(err, "making query")
}
if err := writeTable(queryResponse, cmd.writeOptions, cmd.output, cmd.Stdout, cmd.Stderr); err != nil {
@ -436,7 +450,7 @@ func (cmd *Command) setupHistory() {
if err != nil {
cmd.Errorf("Creating directory for history: %v\n", err)
} else {
historyPath = filepath.Join(historyDir, "cli_history")
historyPath = filepath.Join(historyDir, "fbsql_history")
}
}
cmd.historyPath = historyPath

16
cli/errors.go Normal file
View file

@ -0,0 +1,16 @@
package cli
import (
"github.com/featurebasedb/featurebase/v3/errors"
)
const (
ErrOrganizationRequired errors.Code = "OrganizationRequired"
)
func NewErrOrganizationRequired() error {
return errors.New(
ErrOrganizationRequired,
"organization required",
)
}

View file

@ -17,10 +17,11 @@ import (
"github.com/featurebasedb/featurebase/v3/errors"
)
// action is used to indicate how CLICommand should respond after execution a
// given metaCommand. For example, an action of type "reset" tells CLICommand
// that the buffer has been reset and it needs to change its user prompt.
type action string
// responseAction is used to indicate how CLICommand should respond after
// executing a given metaCommand. For example, a responseAction of type "reset"
// tells CLICommand that the buffer has been reset and it needs to change its
// user prompt.
type responseAction string
const (
actionNone = ""
@ -30,7 +31,7 @@ const (
// metaCommand is the interface for any type responding to a "\" meta-command.
type metaCommand interface {
execute(cmd *Command) (action, error)
execute(cmd *Command) (responseAction, error)
}
// Ensure type implements interface.
@ -45,6 +46,7 @@ var _ metaCommand = (*metaHelp)(nil)
var _ metaCommand = (*metaInclude)(nil)
var _ metaCommand = (*metaListDatabases)(nil)
var _ metaCommand = (*metaListTables)(nil)
var _ metaCommand = (*metaListViews)(nil)
var _ metaCommand = (*metaOrg)(nil)
var _ metaCommand = (*metaOutput)(nil)
var _ metaCommand = (*metaPrint)(nil)
@ -73,7 +75,7 @@ func newMetaBang(args []string) *metaBang {
}
}
func (m *metaBang) execute(cmd *Command) (action, error) {
func (m *metaBang) execute(cmd *Command) (responseAction, error) {
if len(m.args) == 0 {
return actionNone, errors.Errorf("meta command '!' requires at least one argument")
}
@ -97,7 +99,7 @@ func newMetaBorder(args []string) *metaBorder {
}
}
func (m *metaBorder) execute(cmd *Command) (action, error) {
func (m *metaBorder) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
// pass
@ -131,12 +133,23 @@ func newMetaChangeDirectory(args []string) *metaChangeDirectory {
}
}
func (m *metaChangeDirectory) execute(cmd *Command) (action, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'cd' requires exactly one argument")
func (m *metaChangeDirectory) execute(cmd *Command) (responseAction, error) {
var dir string
switch len(m.args) {
case 0:
if d, err := os.UserHomeDir(); err != nil {
return actionNone, errors.Wrapf(err, "getting home directory")
} else {
dir = d
}
case 1:
dir = m.args[0]
default:
return actionNone, errors.Errorf("meta command 'cd' takes zero or one argument")
}
err := cmd.workingDir.cd(m.args[0])
return actionNone, errors.Wrap(err, "running cd command")
err := cmd.workingDir.cd(dir)
return actionNone, errors.Wrapf(err, "changing directory to: %s", dir)
}
// ////////////////////////////////////////////////////////////////////////////
@ -152,7 +165,7 @@ func newMetaConnect(args []string) *metaConnect {
}
}
func (m *metaConnect) execute(cmd *Command) (action, error) {
func (m *metaConnect) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.Printf(cmd.connectionMessage())
@ -160,7 +173,6 @@ func (m *metaConnect) execute(cmd *Command) (action, error) {
case 1:
err := cmd.connectToDatabase(m.args[0])
return actionNone, err
default:
return actionNone, errors.Errorf("meta command 'connect' takes zero or one argument")
}
@ -179,11 +191,11 @@ func newMetaEcho(args []string) *metaEcho {
}
}
func (m *metaEcho) execute(cmd *Command) (action, error) {
func (m *metaEcho) execute(cmd *Command) (responseAction, error) {
return echo(m.args, cmd.Stdout)
}
func echo(args []string, w io.Writer) (action, error) {
func echo(args []string, w io.Writer) (responseAction, error) {
switch len(args) {
case 0:
w.Write([]byte("\n"))
@ -220,7 +232,7 @@ func newMetaExpanded(args []string) *metaExpanded {
}
}
func (m *metaExpanded) execute(cmd *Command) (action, error) {
func (m *metaExpanded) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.expanded = !cmd.writeOptions.expanded
@ -259,7 +271,7 @@ func newMetaFile(args []string) *metaFile {
}
}
func (m *metaFile) execute(cmd *Command) (action, error) {
func (m *metaFile) execute(cmd *Command) (responseAction, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'file' requires exactly one argument")
}
@ -294,7 +306,7 @@ func newMetaHelp(args []string) *metaHelp {
}
}
func (m *metaHelp) execute(cmd *Command) (action, error) {
func (m *metaHelp) execute(cmd *Command) (responseAction, error) {
helpText := `General
\q[uit] quit psql
\watch [SEC] execute query every SEC seconds
@ -305,21 +317,21 @@ Help
Query Buffer
\p[rint] show the contents of the query buffer
\r[eset] reset (clear) the query buffer
\w FILE write query buffer to file
\w[rite] FILE write query buffer to file
Input/Output
\echo [-n] [STRING] write string to standard output (-n for no newline)
\file ... reference a local file to stream to the server
\i[nclude] FILE execute commands from file
\o [FILE] send all query results to file
\o[ut] [FILE] send all query results to file
\qecho [-n] [STRING] write string to \o output stream (-n for no newline)
\warn [-n] [STRING] write string to standard error (-n for no newline)
Informational
\d list tables and views
\d list tables
\dt list tables
\dv list views
\l list databases
\l[ist] list databases
Formatting
\pset [NAME [VALUE]] set table output option
@ -358,7 +370,7 @@ func newMetaInclude(args []string) *metaInclude {
}
}
func (m *metaInclude) execute(cmd *Command) (action, error) {
func (m *metaInclude) execute(cmd *Command) (responseAction, error) {
if len(m.args) != 1 {
return actionNone, errors.Errorf("meta command 'include' requires exactly one argument")
}
@ -366,7 +378,7 @@ func (m *metaInclude) execute(cmd *Command) (action, error) {
return executeFile(cmd, m.args[0])
}
func executeFile(cmd *Command, fileName string) (action, error) {
func executeFile(cmd *Command, fileName string) (responseAction, error) {
file, err := os.Open(fileName)
if err != nil {
return actionNone, errors.Wrapf(err, "opening file: %s", fileName)
@ -385,8 +397,6 @@ func executeFile(cmd *Command, fileName string) (action, error) {
qps, mcs, err := splitter.split(line)
if err != nil {
return actionNone, errors.Wrapf(err, "splitting lines")
} else if len(mcs) > 0 {
return actionNone, errors.Errorf("include does not support meta-commands")
}
for i := range qps {
@ -398,6 +408,18 @@ func executeFile(cmd *Command, fileName string) (action, error) {
}
}
}
for i := range mcs {
action, err := mcs[i].execute(cmd)
if err != nil {
return actionNone, errors.Wrap(err, "executing meta command")
}
switch action {
case actionQuit:
close(cmd.quit)
return action, nil
}
}
}
if err := sc.Err(); err != nil {
return actionNone, errors.Wrapf(err, "scanning file: %s", fileName)
@ -415,7 +437,7 @@ func newMetaListDatabases() *metaListDatabases {
return &metaListDatabases{}
}
func (m *metaListDatabases) execute(cmd *Command) (action, error) {
func (m *metaListDatabases) execute(cmd *Command) (responseAction, error) {
qry := []queryPart{
newPartRaw("SHOW DATABASES"),
}
@ -436,7 +458,7 @@ func newMetaListTables() *metaListTables {
return &metaListTables{}
}
func (m *metaListTables) execute(cmd *Command) (action, error) {
func (m *metaListTables) execute(cmd *Command) (responseAction, error) {
qry := []queryPart{
newPartRaw("SHOW TABLES"),
}
@ -448,6 +470,27 @@ func (m *metaListTables) execute(cmd *Command) (action, error) {
return actionReset, nil
}
// ////////////////////////////////////////////////////////////////////////////
// list views (dv)
// ////////////////////////////////////////////////////////////////////////////
type metaListViews struct{}
func newMetaListViews() *metaListViews {
return &metaListViews{}
}
func (m *metaListViews) execute(cmd *Command) (responseAction, error) {
qry := []queryPart{
newPartRaw("SELECT * FROM fb_views"),
}
if err := cmd.executeAndWriteQuery(qry); err != nil {
return actionNone, errors.Wrap(err, "executing query")
}
return actionReset, nil
}
// ////////////////////////////////////////////////////////////////////////////
// org
// ////////////////////////////////////////////////////////////////////////////
@ -461,7 +504,7 @@ func newMetaOrg(args []string) *metaOrg {
}
}
func (m *metaOrg) execute(cmd *Command) (action, error) {
func (m *metaOrg) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
@ -490,7 +533,7 @@ func newMetaOutput(args []string) *metaOutput {
}
}
func (m *metaOutput) execute(cmd *Command) (action, error) {
func (m *metaOutput) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
// Close cmd.output (if closable).
@ -532,7 +575,7 @@ func newMetaPrint() *metaPrint {
return &metaPrint{}
}
func (m *metaPrint) execute(cmd *Command) (action, error) {
func (m *metaPrint) execute(cmd *Command) (responseAction, error) {
cmd.Printf(cmd.buffer.print() + "\n")
return actionNone, nil
}
@ -571,7 +614,7 @@ tuples_only %s
}
func (m *metaPSet) execute(cmd *Command) (action, error) {
func (m *metaPSet) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
m.print(cmd)
@ -581,10 +624,10 @@ func (m *metaPSet) execute(cmd *Command) (action, error) {
case "border":
sub := newMetaBorder(m.args[1:])
return sub.execute(cmd)
case "expanded":
case "expanded", "x":
sub := newMetaExpanded(m.args[1:])
return sub.execute(cmd)
case "tuples_only":
case "tuples_only", "t":
sub := newMetaTuplesOnly(m.args[1:])
return sub.execute(cmd)
default:
@ -608,7 +651,7 @@ func newMetaQEcho(args []string) *metaQEcho {
}
}
func (m *metaQEcho) execute(cmd *Command) (action, error) {
func (m *metaQEcho) execute(cmd *Command) (responseAction, error) {
return echo(m.args, cmd.output)
}
@ -621,7 +664,7 @@ func newMetaQuit() *metaQuit {
return &metaQuit{}
}
func (m *metaQuit) execute(cmd *Command) (action, error) {
func (m *metaQuit) execute(cmd *Command) (responseAction, error) {
return actionQuit, nil
}
@ -634,7 +677,7 @@ func newMetaReset() *metaReset {
return &metaReset{}
}
func (m *metaReset) execute(cmd *Command) (action, error) {
func (m *metaReset) execute(cmd *Command) (responseAction, error) {
cmd.Printf(cmd.buffer.reset())
return actionReset, nil
}
@ -652,7 +695,7 @@ func newMetaSet(args []string) *metaSet {
}
}
func (m *metaSet) execute(cmd *Command) (action, error) {
func (m *metaSet) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
// Sort the variables before printing them.
@ -692,7 +735,7 @@ func newMetaTiming(args []string) *metaTiming {
}
}
func (m *metaTiming) execute(cmd *Command) (action, error) {
func (m *metaTiming) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.timing = !cmd.writeOptions.timing
@ -731,7 +774,7 @@ func newMetaTuplesOnly(args []string) *metaTuplesOnly {
}
}
func (m *metaTuplesOnly) execute(cmd *Command) (action, error) {
func (m *metaTuplesOnly) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.writeOptions.tuplesOnly = !cmd.writeOptions.tuplesOnly
@ -770,7 +813,7 @@ func newMetaUnset(args []string) *metaUnset {
}
}
func (m *metaUnset) execute(cmd *Command) (action, error) {
func (m *metaUnset) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.Printf("\\unset: missing required argument\n")
@ -799,7 +842,7 @@ func newMetaWarn(args []string) *metaWarn {
}
}
func (m *metaWarn) execute(cmd *Command) (action, error) {
func (m *metaWarn) execute(cmd *Command) (responseAction, error) {
return echo(m.args, cmd.Stderr)
}
@ -816,7 +859,7 @@ func newMetaWatch(args []string) *metaWatch {
}
}
func (m *metaWatch) execute(cmd *Command) (action, error) {
func (m *metaWatch) execute(cmd *Command) (responseAction, error) {
period := 2 * time.Second
qry := cmd.buffer.lastQuery
@ -870,7 +913,7 @@ func newMetaWrite(args []string) *metaWrite {
}
}
func (m *metaWrite) execute(cmd *Command) (action, error) {
func (m *metaWrite) execute(cmd *Command) (responseAction, error) {
switch len(m.args) {
case 0:
cmd.Errorf(`\w: missing required argument` + "\n")
@ -948,6 +991,8 @@ func splitMetaCommand(in string, replacer *replacer) (metaCommand, error) {
return newMetaConnect(args), nil
case "d", "dt":
return newMetaListTables(), nil
case "dv":
return newMetaListViews(), nil
case "echo":
return newMetaEcho(args), nil
case "file":
@ -956,9 +1001,9 @@ func splitMetaCommand(in string, replacer *replacer) (metaCommand, error) {
return newMetaHelp(args), nil
case "i", "include":
return newMetaInclude(args), nil
case "l":
case "l", "list":
return newMetaListDatabases(), nil
case "o":
case "o", "out":
return newMetaOutput(args), nil
case "org":
return newMetaOrg(args), nil
@ -984,7 +1029,7 @@ func splitMetaCommand(in string, replacer *replacer) (metaCommand, error) {
return newMetaWarn(args), nil
case "watch":
return newMetaWatch(args), nil
case "w":
case "w", "write":
return newMetaWrite(args), nil
case "x":
return newMetaExpanded(args), nil

View file

@ -68,7 +68,10 @@ type serverlessQueryer struct {
}
func (qryr *serverlessQueryer) Query(org string, db string, sql io.Reader) (*featurebase.WireQueryResponse, error) {
// buf := bytes.Buffer{}
if org == "" {
return nil, NewErrOrganizationRequired()
}
url := fmt.Sprintf("%s/queryer/databases/%s/sql", hostPort(qryr.Host, qryr.Port), db)
if db == "" {
url = fmt.Sprintf("%s/queryer/sql", hostPort(qryr.Host, qryr.Port))

View file

@ -48,7 +48,7 @@ func buildFlags(cmd *cobra.Command, cliCmd *cli.Command) {
// Config flags.
flags.StringVarP(&cliCmd.Config.Host, "host", "", cliCmd.Config.Host, "hostname of FeatureBase.")
flags.StringVarP(&cliCmd.Config.Port, "port", "", cliCmd.Config.Port, "port of FeatureBase.")
flags.StringVarP(&cliCmd.Config.Port, "port", "p", cliCmd.Config.Port, "port of FeatureBase.")
flags.StringVar(&cliCmd.Config.HistoryPath, "history-path", cliCmd.Config.HistoryPath, "path for history files.")
flags.StringVar(&cliCmd.Config.OrganizationID, "org-id", cliCmd.Config.OrganizationID, "OrganizationID.")
flags.StringVarP(&cliCmd.Config.Database, "dbname", "d", cliCmd.Config.Database, "Name of the database to connect to.")