Merge branch 'nicktobey-820598b3' into fulghum/dolt-7945

This commit is contained in:
Jason Fulghum
2024-06-04 16:33:45 -07:00
18 changed files with 564 additions and 112 deletions

107
go/Godeps/LICENSES generated
View File

@@ -2287,6 +2287,28 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
= LICENSE.txt 726f1b8f64f7e439b1b12c7cbde7b1427752a00ddea15019e4156465 =
================================================================================
================================================================================
= github.com/davecgh/go-spew licensed under: =
ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
= LICENSE 1df7eb862ea59e064cc5b27e5d88aad979fad02e3755973892829af8 =
================================================================================
================================================================================
= github.com/denisbrodbeck/machineid licensed under: =
@@ -9023,6 +9045,34 @@ THE SOFTWARE.
= LICENSE e721de56f0e19f85d9b7a0e343990895cd9a9689444408cef8f69a86 =
================================================================================
================================================================================
= github.com/stretchr/testify licensed under: =
MIT License
Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
= LICENSE 07f20b96549b71d39ebb2bf1e006f7b2885e3808423818000545119c =
================================================================================
================================================================================
= github.com/tealeg/xlsx licensed under: =
@@ -13115,3 +13165,60 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
= LICENSE 9820a37ca0fcacbc82c8eb2bdd3049706550a4ebf97ad7fde1310dec =
================================================================================
================================================================================
= gopkg.in/yaml.v3 licensed under: =
This project is covered by two different licenses: MIT and Apache.
#### MIT License ####
The following files were ported to Go from C files of libyaml, and thus
are still covered by their original MIT license, with the additional
copyright staring in 2011 when the project was ported over:
apic.go emitterc.go parserc.go readerc.go scannerc.go
writerc.go yamlh.go yamlprivateh.go
Copyright (c) 2006-2010 Kirill Simonov
Copyright (c) 2006-2011 Kirill Simonov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### Apache License ###
All the remaining project files are covered by the Apache license:
Copyright (c) 2011-2019 Canonical Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
= LICENSE 1fcda9aa5c036a1d3975c8c4a007e1b3c05f0e450567d8bdb46a6d61 =
================================================================================

View File

@@ -44,17 +44,21 @@ func NewCliContext(args *argparser.ArgParseResults, config *env.DoltCliConfig, l
return nil, errhand.VerboseErrorFromError(errors.New("Invariant violated. args, config, and latebind must be non nil."))
}
return LateBindCliContext{globalArgs: args, config: config, bind: latebind}, nil
return LateBindCliContext{globalArgs: args, config: config, activeContext: &QueryistContext{}, bind: latebind}, nil
}
type QueryistContext struct {
sqlCtx *sql.Context
qryist *Queryist
}
// LateBindCliContext is a struct that implements CliContext. Its primary purpose is to wrap the global arguments and
// provide an implementation of the QueryEngine function. This instance is stateful to ensure that the Queryist is only
// created once.
type LateBindCliContext struct {
globalArgs *argparser.ArgParseResults
queryist Queryist
sqlCtx *sql.Context
config *env.DoltCliConfig
globalArgs *argparser.ArgParseResults
config *env.DoltCliConfig
activeContext *QueryistContext
bind LateBindQueryist
}
@@ -68,8 +72,8 @@ func (lbc LateBindCliContext) GlobalArgs() *argparser.ArgParseResults {
// LateBindQueryist is made, and caches the result. Note that if this is called twice, the closer function returns will
// be nil, callers should check if is nil.
func (lbc LateBindCliContext) QueryEngine(ctx context.Context) (Queryist, *sql.Context, func(), error) {
if lbc.queryist != nil {
return lbc.queryist, lbc.sqlCtx, nil, nil
if lbc.activeContext != nil && lbc.activeContext.qryist != nil && lbc.activeContext.sqlCtx != nil {
return *lbc.activeContext.qryist, lbc.activeContext.sqlCtx, nil, nil
}
qryist, sqlCtx, closer, err := lbc.bind(ctx)
@@ -77,8 +81,8 @@ func (lbc LateBindCliContext) QueryEngine(ctx context.Context) (Queryist, *sql.C
return nil, nil, nil, err
}
lbc.queryist = qryist
lbc.sqlCtx = sqlCtx
lbc.activeContext.qryist = &qryist
lbc.activeContext.sqlCtx = sqlCtx
return qryist, sqlCtx, closer, nil
}

View File

@@ -102,8 +102,10 @@ func generateAddSql(apr *argparser.ArgParseResults) string {
// Exec executes the command
func (cmd AddCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateAddArgParser()
helpPr, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, addDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, helpPr)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, addDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -105,8 +105,10 @@ func (cmd BranchCmd) EventType() eventsapi.ClientEventType {
// Exec executes the command
func (cmd BranchCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, branchDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, branchDocs)
if terminate {
return status
}
queryEngine, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -86,12 +86,14 @@ func (cmd CheckoutCmd) EventType() eventsapi.ClientEventType {
// Exec executes the command
func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateCheckoutArgParser()
helpPrt, usagePrt := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, checkoutDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, helpPrt)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, checkoutDocs)
if terminate {
return status
}
queryEngine, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
if closeFunc != nil {
defer closeFunc()
@@ -110,7 +112,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
// won't be as nice.
branchOrTrack := apr.Contains(cli.CheckoutCreateBranch) || apr.Contains(cli.CreateResetBranch) || apr.Contains(cli.TrackFlag)
if (branchOrTrack && apr.NArg() > 1) || (!branchOrTrack && apr.NArg() == 0) {
usagePrt()
usage()
return 1
}
@@ -122,7 +124,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
branchName, _ = apr.GetValue(cli.CreateResetBranch)
} else if apr.Contains(cli.TrackFlag) {
if apr.NArg() > 0 {
usagePrt()
usage()
return 1
}
remoteAndBranchName, _ := apr.GetValue(cli.TrackFlag)
@@ -133,7 +135,7 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
sqlQuery, err := generateCheckoutSql(args)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
rows, err := GetRowsForSql(queryEngine, sqlCtx, sqlQuery)
@@ -141,45 +143,50 @@ func (cmd CheckoutCmd) Exec(ctx context.Context, commandStr string, args []strin
if err != nil {
// In fringe cases the server can't start because the default branch doesn't exist, `dolt checkout <existing branch>`
// offers an escape hatch.
if !branchOrTrack && strings.Contains(err.Error(), "cannot resolve default branch head for database") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
if dEnv != nil {
// TODO - This error handling is not great.
if !branchOrTrack && strings.Contains(err.Error(), "cannot resolve default branch head for database") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
return 0
}
return 0
}
return HandleVErrAndExitCode(handleErrors(branchName, err), usagePrt)
return HandleVErrAndExitCode(handleErrors(branchName, err), usage)
}
if len(rows) != 1 {
return HandleVErrAndExitCode(errhand.BuildDError("expected 1 row response from %s, got %d", sqlQuery, len(rows)).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("expected 1 row response from %s, got %d", sqlQuery, len(rows)).Build(), usage)
}
if len(rows[0]) < 2 {
return HandleVErrAndExitCode(errhand.BuildDError("no 'message' field in response from %s", sqlQuery).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("no 'message' field in response from %s", sqlQuery).Build(), usage)
}
var message string
if message, ok = rows[0][1].(string); !ok {
return HandleVErrAndExitCode(errhand.BuildDError("expected string value for 'message' field in response from %s ", sqlQuery).Build(), usagePrt)
return HandleVErrAndExitCode(errhand.BuildDError("expected string value for 'message' field in response from %s ", sqlQuery).Build(), usage)
}
if message != "" {
cli.Println(message)
}
if strings.Contains(message, "Switched to branch") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
// This command doesn't modify `dEnv` which could break tests that call multiple commands in sequence.
// We must reload it so that it includes changes to the repo state.
err = dEnv.ReloadRepoState()
if err != nil {
return 1
if dEnv != nil {
if strings.Contains(message, "Switched to branch") {
err := saveHeadBranch(dEnv.FS, branchName)
if err != nil {
cli.PrintErr(err)
return 1
}
// This command doesn't modify `dEnv` which could break tests that call multiple commands in sequence.
// We must reload it so that it includes changes to the repo state.
err = dEnv.ReloadRepoState()
if err != nil {
return 1
}
}
}

View File

@@ -104,11 +104,13 @@ func (cmd CommitCmd) Exec(ctx context.Context, commandStr string, args []string,
// (e.g. because --skip-empty was specified as an argument).
func performCommit(ctx context.Context, commandStr string, args []string, cliCtx cli.CliContext, temporaryDEnv *env.DoltEnv) (int, bool) {
ap := cli.CreateCommitArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, commitDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, commitDocs)
if terminate {
return status, false
}
if err := cli.VerifyCommitArgs(apr); err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), help), false
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage), false
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)

View File

@@ -185,10 +185,12 @@ func (cmd DiffCmd) RequiresRepo() bool {
}
// Exec executes the command
func (cmd DiffCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
func (cmd DiffCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, diffDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, diffDocs)
if terminate {
return status
}
verr := cmd.validateArgs(apr)
if verr != nil {

View File

@@ -99,8 +99,10 @@ func (cmd LogCmd) Exec(ctx context.Context, commandStr string, args []string, dE
func (cmd LogCmd) logWithLoggerFunc(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, logDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, logDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -94,8 +94,10 @@ func (cmd MergeCmd) RequiresRepo() bool {
func (cmd MergeCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateMergeArgParser()
ap.SupportsFlag(cli.NoJsonMergeFlag, "", "Do not attempt to automatically resolve multiple changes to the same JSON value, report a conflict instead.")
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, mergeDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, mergeDocs)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -89,10 +89,12 @@ func (cmd ResetCmd) RequiresRepo() bool {
}
// Exec executes the command
func (cmd ResetCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
func (cmd ResetCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreateResetArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, resetDocContent, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, usage, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, resetDocContent)
if terminate {
return status
}
queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {

View File

@@ -21,6 +21,7 @@ import (
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
@@ -96,7 +97,7 @@ const (
welcomeMsg = `# Welcome to the DoltSQL shell.
# Statements must be terminated with ';'.
# "exit" or "quit" (or Ctrl-D) to exit.`
# "exit" or "quit" (or Ctrl-D) to exit. "/help;" for help.`
)
// TODO: get rid of me, use a real integration point to define system variables
@@ -260,7 +261,7 @@ func (cmd SqlCmd) Exec(ctx context.Context, commandStr string, args []string, dE
}
if isTty {
err := execShell(sqlCtx, queryist, format)
err := execShell(sqlCtx, queryist, format, cliCtx)
if err != nil {
return sqlHandleVErrAndExitCode(queryist, errhand.VerboseErrorFromError(err), usage)
}
@@ -685,7 +686,7 @@ func buildBatchSqlErr(stmtStartLine int, query string, err error) error {
// execShell starts a SQL shell. Returns when the user exits the shell. The Root of the sqlEngine may
// be updated by any queries which were processed.
func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat) error {
func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResultFormat, cliCtx cli.CliContext) error {
_ = iohelp.WriteLine(cli.CliOut, welcomeMsg)
historyFile := filepath.Join(".sqlhistory") // history file written to working dir
@@ -750,65 +751,85 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
return
}
closureFormat := format
// TODO: there's a bug in the readline library when editing multi-line history entries.
// Longer term we need to switch to a new readline library, like in this bug:
// https://github.com/cockroachdb/cockroach/issues/15460
// For now, we store all history entries as single-line strings to avoid the issue.
singleLine := strings.ReplaceAll(query, "\n", " ")
if err := shell.AddHistory(singleLine); err != nil {
// TODO: handle better, like by turning off history writing for the rest of the session
shell.Println(color.RedString(err.Error()))
}
query = strings.TrimSuffix(query, shell.LineTerminator())
// TODO: it would be better to build this into the statement parser rather than special case it here
for _, terminator := range verticalOutputLineTerminators {
if strings.HasSuffix(query, terminator) {
closureFormat = engine.FormatVertical
}
query = strings.TrimSuffix(query, terminator)
}
cont := true
var nextPrompt string
var multiPrompt string
var sqlSch sql.Schema
var rowIter sql.RowIter
func() {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
if sqlSch, rowIter, err = processQuery(sqlCtx, query, qryist); err != nil {
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
switch closureFormat {
case engine.FormatTabular, engine.FormatVertical:
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter)
default:
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter)
}
re := regexp.MustCompile(`\s*/(.*)`)
matches := re.FindStringSubmatch(query)
// If the query starts with a slash, it's a shell command. We don't want to print the query in that case.
if len(matches) > 1 {
func() {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
slashCmd := matches[1]
err := handleSlashCommand(sqlCtx, slashCmd, cliCtx)
if err != nil {
shell.Println(color.RedString(err.Error()))
}
nextPrompt, multiPrompt = postCommandUpdate(sqlCtx, qryist)
}()
} else {
closureFormat := format
// TODO: there's a bug in the readline library when editing multi-line history entries.
// Longer term we need to switch to a new readline library, like in this bug:
// https://github.com/cockroachdb/cockroach/issues/15460
// For now, we store all history entries as single-line strings to avoid the issue.
singleLine := strings.ReplaceAll(query, "\n", " ")
if err := shell.AddHistory(singleLine); err != nil {
// TODO: handle better, like by turning off history writing for the rest of the session
shell.Println(color.RedString(err.Error()))
}
db, branch, ok := getDBBranchFromSession(sqlCtx, qryist)
if ok {
sqlCtx.SetCurrentDatabase(db)
query = strings.TrimSuffix(query, shell.LineTerminator())
// TODO: it would be better to build this into the statement parser rather than special case it here
for _, terminator := range verticalOutputLineTerminators {
if strings.HasSuffix(query, terminator) {
closureFormat = engine.FormatVertical
}
query = strings.TrimSuffix(query, terminator)
}
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
nextPrompt, multiPrompt = formattedPrompts(db, branch, dirty)
}()
var sqlSch sql.Schema
var rowIter sql.RowIter
cont = func() bool {
subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
defer stop()
sqlCtx := sql.NewContext(subCtx, sql.WithSession(sqlCtx.Session))
if sqlSch, rowIter, err = processQuery(sqlCtx, query, qryist); err != nil {
verr := formatQueryError("", err)
shell.Println(verr.Verbose())
} else if rowIter != nil {
switch closureFormat {
case engine.FormatTabular, engine.FormatVertical:
err = engine.PrettyPrintResultsExtended(sqlCtx, closureFormat, sqlSch, rowIter)
default:
err = engine.PrettyPrintResults(sqlCtx, closureFormat, sqlSch, rowIter)
}
if err != nil {
shell.Println(color.RedString(err.Error()))
}
}
nextPrompt, multiPrompt = postCommandUpdate(sqlCtx, qryist)
return true
}()
}
if !cont {
return
}
shell.SetPrompt(nextPrompt)
shell.SetMultiPrompt(multiPrompt)
@@ -820,6 +841,20 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
return nil
}
// postCommandUpdate is a helper function that is run after the shell has completed a command. It updates the the database
// if needed, and generates new prompts for the shell (based on the branch and if the workspace is dirty).
func postCommandUpdate(sqlCtx *sql.Context, qryist cli.Queryist) (string, string) {
db, branch, ok := getDBBranchFromSession(sqlCtx, qryist)
if ok {
sqlCtx.SetCurrentDatabase(db)
}
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
return formattedPrompts(db, branch, dirty)
}
// formattedPrompts returns the prompt and multiline prompt for the current session. If the db is empty, the prompt will
// be "> ", otherwise it will be "db> ". If the branch is empty, the multiline prompt will be "-> ", left padded for
// alignment with the prompt.
@@ -892,7 +927,7 @@ func getDBBranchFromSession(sqlCtx *sql.Context, qryist cli.Queryist) (db string
// isDirty returns true if the workspace is dirty, false otherwise. This function _assumes_ you are on a database
// with a branch. If you are not, you will get an error.
func isDirty(sqlCtx *sql.Context, qryist cli.Queryist) (bool, error) {
_, resp, err := qryist.Query(sqlCtx, "select count(table_name) > 0 as dirty from dolt_Status")
_, resp, err := qryist.Query(sqlCtx, "select count(table_name) > 0 as dirty from dolt_status")
if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))

View File

@@ -0,0 +1,181 @@
// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
)
var slashCmds = []cli.Command{
StatusCmd{},
DiffCmd{},
LogCmd{},
AddCmd{},
CommitCmd{},
CheckoutCmd{},
ResetCmd{},
BranchCmd{},
MergeCmd{},
SlashHelp{},
}
// parseSlashCmd parses a command line string into a slice of strings, splitting on spaces, but allowing spaces within
// double quotes. For example, the string `foo "bar baz"` would be parsed into the slice `[]string{"foo", "bar baz"}`.
// This is quick and dirty for slash command prototype, and doesn't try and handle all the crazy edge cases that come
// up with supporting many types of quotes. Also, pretty sure a dangling quote will break it. But it's a start.
func parseSlashCmd(cmd string) []string {
// TODO: determine if we can get rid of the ";" as the terminator for cli commands.
cmd = strings.TrimSuffix(cmd, ";")
cmd = strings.TrimRight(cmd, " \t\n\r\v\f")
cmd = strings.TrimLeft(cmd, " \t\n\r\v\f")
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|\S+`)
cmdWords := r.FindAllString(cmd, -1)
for i := range cmdWords {
if cmdWords[i][0] == '"' {
cmdWords[i] = cmdWords[i][1 : len(cmdWords[i])-1]
cmdWords[i] = strings.ReplaceAll(cmdWords[i], `\"`, `"`)
}
}
if len(cmdWords) == 0 {
return []string{}
}
return cmdWords
}
func handleSlashCommand(sqlCtx *sql.Context, fullCmd string, cliCtx cli.CliContext) error {
cliCmd := parseSlashCmd(fullCmd)
if len(cliCmd) == 0 {
return fmt.Errorf("Empty command. Use `/help;` for help.")
}
subCmd := cliCmd[0]
subCmdArgs := cliCmd[1:]
status := 1
subCmdInst, ok := findSlashCmd(subCmd)
if ok {
status = subCmdInst.Exec(sqlCtx, subCmd, subCmdArgs, nil, cliCtx)
} else {
return fmt.Errorf("Unknown command: %s. Use `/help;` for a list of command.", subCmd)
}
if status != 0 {
return fmt.Errorf("error executing command: %s", cliCmd)
}
return nil
}
type SlashHelp struct{}
func (s SlashHelp) Name() string {
return "help"
}
func (s SlashHelp) Description() string {
return "What you see right now."
}
func (s SlashHelp) Docs() *cli.CommandDocumentation {
return &cli.CommandDocumentation{
CommandStr: "/help",
ShortDesc: "What you see right now.",
LongDesc: "It would seem that you are crying out for help. Please join us on Discord (https://discord.gg/gqr7K4VNKe)!",
Synopsis: []string{},
ArgParser: s.ArgParser(),
}
}
func (s SlashHelp) Exec(ctx context.Context, _ string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
if args != nil && len(args) > 0 {
subCmd := args[0]
subCmdInst, ok := findSlashCmd(subCmd)
if ok {
foo, _ := cli.HelpAndUsagePrinters(subCmdInst.Docs())
foo()
} else {
cli.Println(fmt.Sprintf("Unknown command: %s", subCmd))
}
return 0
}
qryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if closeFunc != nil {
defer closeFunc()
}
if err != nil {
cli.Println(fmt.Sprintf("error getting query engine: %s", err))
return 1
}
prompt := generateHelpPrompt(sqlCtx, qryist)
cli.Println("Dolt SQL Shell Help")
cli.Printf("Default behavior is to interpret SQL statements. (e.g. '%sselect * from my_table;')\n", prompt)
cli.Printf("Dolt CLI commands can be invoked with a leading '/'. (e.g. '%s/status;')\n", prompt)
cli.Println("All statements are terminated with a ';'.")
cli.Println("\nAvailable commands:")
for _, cmdInst := range slashCmds {
cli.Println(fmt.Sprintf(" %10s - %s", cmdInst.Name(), cmdInst.Description()))
}
cli.Printf("\nFor more information on a specific command, type '/help <command>;' (e.g. '%s/help status;')\n", prompt)
moreWords := `
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
Still need assistance? Talk directly to Dolt developers on Discord! https://discord.gg/gqr7K4VNKe
Found a bug? Want additional features? Please let us know! https://github.com/dolthub/dolt/issues
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-`
cli.Println(moreWords)
return 0
}
func generateHelpPrompt(sqlCtx *sql.Context, qryist cli.Queryist) string {
db, branch, _ := getDBBranchFromSession(sqlCtx, qryist)
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}
prompt, _ := formattedPrompts(db, branch, dirty)
return prompt
}
func (s SlashHelp) ArgParser() *argparser.ArgParser {
return &argparser.ArgParser{}
}
func findSlashCmd(cmd string) (cli.Command, bool) {
for _, cmdInst := range slashCmds {
if cmdInst.Name() == cmd {
return cmdInst, true
}
}
return nil, false
}

View File

@@ -100,12 +100,13 @@ func (cmd StatusCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_STATUS
}
// Exec executes the command
func (cmd StatusCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
// parse arguments
ap := cmd.ArgParser()
help, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, statusDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
apr, _, terminate, status := ParseArgsAndPrintHelp(ap, commandStr, args, statusDocs)
if terminate {
return status
}
showIgnoredTables := apr.Contains(cli.ShowIgnoredFlag)
// configure SQL engine
@@ -656,7 +657,9 @@ and have %v and %v different commits each, respectively.
}
func handleStatusVErr(err error) int {
cli.PrintErrln(errhand.VerboseErrorFromError(err).Verbose())
if err != argparser.ErrHelp {
cli.PrintErrln(errhand.VerboseErrorFromError(err).Verbose())
}
return 1
}

View File

@@ -774,6 +774,23 @@ func getHashOf(queryist cli.Queryist, sqlCtx *sql.Context, ref string) (string,
return rows[0][0].(string), nil
}
func ParseArgsAndPrintHelp(
ap *argparser.ArgParser,
commandStr string,
args []string,
docs cli.CommandDocumentationContent) (apr *argparser.ArgParseResults, usage cli.UsagePrinter, terminate bool, exitStatus int) {
helpPrt, usagePrt := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, docs, ap))
var err error
apr, err = cli.ParseArgs(ap, args, helpPrt)
if err != nil {
if err == argparser.ErrHelp {
return nil, usagePrt, true, 0
}
return nil, usagePrt, true, HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usagePrt)
}
return apr, usagePrt, false, 0
}
func HandleVErrAndExitCode(verr errhand.VerboseError, usage cli.UsagePrinter) int {
if verr != nil {
if msg := verr.Verbose(); strings.TrimSpace(msg) != "" {

View File

@@ -72,7 +72,8 @@ func Start() *Pager {
// -S ... Chop (truncate) long lines rather than wrapping.
// -R ... Output "raw" control characters.
// -X ... Don't use termcap init/deinit strings.
cmd = exec.Command(lessPath, "-FSRX")
// -d ... Don't complain about dumb terminals.
cmd = exec.Command(lessPath, "-FSRXd")
}
stdin, stdout, err := os.Pipe()

View File

@@ -0,0 +1,69 @@
#!/usr/bin/expect
set timeout 5
set env(NO_COLOR) 1
proc expect_with_defaults {pattern action} {
expect {
-re $pattern {
# puts "Matched pattern: $pattern"
eval $action
}
timeout {
puts "<<Timeout>>";
exit 1
}
eof {
puts "<<End of File reached>>";
exit 1
}
failed {
puts "<<Failed>>";
exit 1
}
}
}
proc expect_with_defaults_2 {patternA patternB action} {
expect {
-re $patternA {
# puts "Matched pattern: $patternA"
exp_continue
}
-re $patternB {
# puts "Matched pattern: $patternB"
eval $action
}
timeout {
puts "<<Timeout>>";
exit 1
}
eof {
puts "<<End of File reached>>";
exit 1
}
failed {
puts "<<Failed>>";
exit 1
}
}
}
spawn dolt sql
expect_with_defaults {dolt-repo-[0-9]+/main\*> } { send "/commit -A -m \"sql-shell-slash-cmds commit\";\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main> } { send "/log -n 1;\r"; }
expect_with_defaults_2 {sql-shell-slash-cmds commit} {dolt-repo-[0-9]+/main> } { send "/status;\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main> } { send "/reset HEAD~1;\r"; }
expect_with_defaults {dolt-repo-[0-9]+/main\*> } { send "/diff;\r"; }
expect_with_defaults_2 {diff --dolt a/tbl b/tbl} {dolt-repo-[0-9]+/main\*> } {send "quit\r";}
expect eof
exit

View File

@@ -67,6 +67,18 @@ teardown() {
[[ "$output" =~ "+---------------------" ]] || false
}
# bats test_tags=no_lambda
@test "sql-shell: sql shell executes slash commands" {
skiponwindows "Need to install expect and make this script work on windows."
if [ "$SQL_ENGINE" = "remote-engine" ]; then
skip "Current test setup results in remote calls having a clean branch, where this expect script expects dirty."
fi
run $BATS_TEST_DIRNAME/sql-shell-slash-cmds.expect
echo "$output"
[ "$status" -eq 0 ]
}
# bats test_tags=no_lambda
@test "sql-shell: sql shell prompt updates" {
skiponwindows "Need to install expect and make this script work on windows."

View File

@@ -13,6 +13,8 @@ teardown() {
# that have nothing to do with product functionality directly.
@test "validation: no test symbols in binary" {
skip "temporarily disabled while we clean up the testify dependency coming in from GMS"
run grep_for_testify
[ "$output" = "" ]
}