mirror of
https://github.com/dolthub/dolt.git
synced 2026-05-02 03:10:42 -05:00
433 lines
12 KiB
Go
433 lines
12 KiB
Go
// Copyright 2025 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 (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/analyzer"
|
|
"github.com/pkg/profile"
|
|
"github.com/sirupsen/logrus"
|
|
textunicode "golang.org/x/text/encoding/unicode"
|
|
"golang.org/x/text/transform"
|
|
|
|
"github.com/dolthub/dolt/go/cmd/dolt/cli"
|
|
"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
|
|
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
|
"github.com/dolthub/dolt/go/libraries/utils/argparser"
|
|
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
|
|
)
|
|
|
|
type DebugCmd struct {
|
|
VersionStr string
|
|
}
|
|
|
|
const defaultDebugTime = 10
|
|
|
|
// The SQL shell installs its own signal handlers so that you can cancel a running query without ending the entire
|
|
// process
|
|
func (cmd DebugCmd) InstallsSignalHandlers() bool {
|
|
return true
|
|
}
|
|
|
|
var _ cli.SignalCommand = DebugCmd{}
|
|
|
|
// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
|
|
func (cmd DebugCmd) Name() string {
|
|
return "debug"
|
|
}
|
|
|
|
// Description returns a description of the command
|
|
func (cmd DebugCmd) Description() string {
|
|
return "Run a query in profile and trace mode"
|
|
}
|
|
|
|
func (cmd DebugCmd) Docs() *cli.CommandDocumentation {
|
|
ap := cmd.ArgParser()
|
|
return cli.NewCommandDocumentation(sqlDocs, ap)
|
|
}
|
|
|
|
func (cmd DebugCmd) ArgParser() *argparser.ArgParser {
|
|
ap := argparser.NewArgParserWithMaxArgs(cmd.Name(), 0)
|
|
ap.SupportsString(QueryFlag, "q", "SQL query to run", "Runs a single query and exits.")
|
|
ap.SupportsString(FormatFlag, "r", "result output format", "How to format result output. Valid values are tabular, csv, json, vertical, and parquet. Defaults to tabular.")
|
|
ap.SupportsFlag(continueFlag, "c", "Continue running queries on an error. Used for batch mode only.")
|
|
ap.SupportsString(fileInputFlag, "f", "input file", "Execute statements from the file given.")
|
|
ap.SupportsInt(timeFlag, "t", "benchmark time", "Execute for at least time seconds.")
|
|
ap.SupportsString(outputFlag, "o", "output dir", "Result directory (Defaults to temporary director)")
|
|
return ap
|
|
}
|
|
|
|
// EventType returns the type of the event to log
|
|
func (cmd DebugCmd) EventType() eventsapi.ClientEventType {
|
|
return eventsapi.ClientEventType_SQL
|
|
}
|
|
|
|
// Exec executes the command
|
|
// Unlike other commands, sql doesn't set a new working root directly, as the SQL layer updates the working set as
|
|
// necessary when committing work.
|
|
func (cmd DebugCmd) 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, sqlDocs, ap))
|
|
apr, err := ap.Parse(args)
|
|
if err != nil {
|
|
if errors.Is(err, argparser.ErrHelp) {
|
|
help()
|
|
return 0
|
|
}
|
|
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
|
|
}
|
|
|
|
// We need a username and password for many SQL commands, so set defaults if they don't exist
|
|
dEnv.Config.SetFailsafes(env.DefaultFailsafeConfig)
|
|
|
|
format := engine.FormatTabular
|
|
if formatSr, ok := apr.GetValue(FormatFlag); ok {
|
|
var verr errhand.VerboseError
|
|
format, verr = GetResultFormat(formatSr)
|
|
if verr != nil {
|
|
return HandleVErrAndExitCode(verr, usage)
|
|
}
|
|
}
|
|
|
|
// restrict LOAD FILE invocations to current directory
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
wd = "/dev/null"
|
|
}
|
|
err = sql.SystemVariables.AssignValues(map[string]interface{}{
|
|
"secure_file_priv": wd,
|
|
})
|
|
if err != nil {
|
|
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
|
|
}
|
|
|
|
queryist, err := cliCtx.QueryEngine(ctx)
|
|
if err != nil {
|
|
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
|
|
}
|
|
|
|
if queryist.IsRemote {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("cannot run debug mode on a pre-existing server").Build(), usage)
|
|
}
|
|
sqlEng, ok := queryist.Queryist.(*engine.SqlEngine)
|
|
if !ok {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("unexpected program state: Queryist is not a *SqlEngine, but is also not remote").Build(), usage)
|
|
}
|
|
|
|
_, continueOnError := apr.GetValue(continueFlag)
|
|
outDir, outputDirSpecified := apr.GetValue(outputFlag)
|
|
if !outputDirSpecified {
|
|
outDir, err = os.MkdirTemp("", "dolt-debug-*")
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("couldn't create tempdir %s", err.Error()).Build(), usage)
|
|
}
|
|
} else {
|
|
err := os.Mkdir(outDir, os.ModePerm)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("failed to create output directory %s", err.Error()).Build(), usage)
|
|
}
|
|
}
|
|
|
|
if outputDirSpecified {
|
|
defer func() {
|
|
file, err := os.Create(outDir + ".tar.gz")
|
|
if err != nil {
|
|
cli.Println("failed to create output file " + err.Error())
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
gzipWriter := gzip.NewWriter(file)
|
|
defer gzipWriter.Close()
|
|
|
|
tarWriter := tar.NewWriter(gzipWriter)
|
|
defer tarWriter.Close()
|
|
|
|
if err := filepath.WalkDir(outDir, func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
return addFileToTar(tarWriter, path)
|
|
}); err != nil {
|
|
cli.Println("failed to create output tar " + err.Error())
|
|
}
|
|
|
|
cli.Println("zipped results in: " + outDir + ".tar.gz")
|
|
}()
|
|
}
|
|
|
|
runTime := apr.GetIntOrDefault(timeFlag, defaultDebugTime)
|
|
|
|
var input io.Reader = os.Stdin
|
|
if query, queryOK := apr.GetValue(QueryFlag); queryOK {
|
|
input = bytes.NewBuffer([]byte(query))
|
|
} else if fileInput, ok := apr.GetValue(fileInputFlag); ok {
|
|
input, err = os.OpenFile(fileInput, os.O_RDONLY, os.ModePerm)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("couldn't open file %s", fileInput).Build(), usage)
|
|
}
|
|
info, err := os.Stat(fileInput)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("couldn't get file size %s", fileInput).Build(), usage)
|
|
}
|
|
|
|
input = transform.NewReader(input, textunicode.BOMOverride(transform.Nop))
|
|
|
|
// initialize fileReadProg global variable if there is a file to process queries from
|
|
fileReadProg = &fileReadProgress{bytesRead: 0, totalBytes: info.Size(), printed: 0, displayStrLen: 0}
|
|
defer fileReadProg.close()
|
|
}
|
|
|
|
queryFile, err := os.Create(filepath.Join(outDir, "input.sql"))
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("couldn't create file %s", err.Error()).Build(), usage)
|
|
}
|
|
defer queryFile.Close()
|
|
|
|
input = bufio.NewReader(transform.NewReader(input, textunicode.BOMOverride(transform.Nop)))
|
|
_, err = io.Copy(queryFile, input)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("couldn't copy input sql %s", err.Error()).Build(), usage)
|
|
}
|
|
_, err = queryFile.Seek(0, 0)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.BuildDError("seek input sql %s", err.Error()).Build(), usage)
|
|
}
|
|
|
|
err = debugAnalyze(queryist.Context, outDir, sqlEng, queryFile)
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.VerboseErrorFromError(err), usage)
|
|
}
|
|
|
|
debugFile, err := os.Create(filepath.Join(outDir, "exec.txt"))
|
|
if err != nil {
|
|
if err != nil {
|
|
return sqlHandleVErrAndExitCode(queryist.Queryist, errhand.VerboseErrorFromError(err), usage)
|
|
}
|
|
}
|
|
defer debugFile.Close()
|
|
|
|
func() {
|
|
defer profile.Start(profile.ProfilePath(outDir), profile.CPUProfile).Stop()
|
|
cli.Println("starting cpu profile...")
|
|
|
|
origStdout := cli.CliOut
|
|
origStderr := cli.CliErr
|
|
cli.CliOut = debugFile
|
|
cli.CliErr = debugFile
|
|
defer func() {
|
|
cli.CliOut = origStdout
|
|
cli.CliErr = origStderr
|
|
}()
|
|
|
|
var done bool
|
|
wait := time.Tick(time.Duration(runTime) * time.Second)
|
|
for !done {
|
|
select {
|
|
case <-wait:
|
|
done = true
|
|
default:
|
|
execDebugMode(queryist.Context, sqlEng, queryFile, continueOnError, format)
|
|
}
|
|
}
|
|
}()
|
|
|
|
func() {
|
|
defer profile.Start(profile.ProfilePath(outDir), profile.MemProfile).Stop()
|
|
cli.Println("starting mem profile...")
|
|
|
|
origStdout := cli.CliOut
|
|
origStderr := cli.CliErr
|
|
cli.CliOut = debugFile
|
|
cli.CliErr = debugFile
|
|
defer func() {
|
|
cli.CliOut = origStdout
|
|
cli.CliErr = origStderr
|
|
}()
|
|
|
|
var done bool
|
|
wait := time.Tick(time.Duration(runTime) * time.Second)
|
|
for !done {
|
|
select {
|
|
case <-wait:
|
|
done = true
|
|
default:
|
|
execDebugMode(queryist.Context, sqlEng, queryFile, continueOnError, format)
|
|
}
|
|
}
|
|
}()
|
|
|
|
func() {
|
|
defer profile.Start(profile.ProfilePath(outDir), profile.TraceProfile).Stop()
|
|
cli.Println("starting trace profile...")
|
|
|
|
origStdout := cli.CliOut
|
|
origStderr := cli.CliErr
|
|
cli.CliOut = debugFile
|
|
cli.CliErr = debugFile
|
|
defer func() {
|
|
cli.CliOut = origStdout
|
|
cli.CliErr = origStderr
|
|
}()
|
|
|
|
var done bool
|
|
wait := time.Tick(time.Duration(runTime) * time.Second)
|
|
for !done {
|
|
select {
|
|
case <-wait:
|
|
done = true
|
|
default:
|
|
execDebugMode(queryist.Context, sqlEng, queryFile, continueOnError, format)
|
|
}
|
|
}
|
|
}()
|
|
|
|
defer cli.Printf("debug results in: %s\n", outDir)
|
|
|
|
return 0
|
|
}
|
|
|
|
func addFileToTar(tarWriter *tar.Writer, filePath string) error {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
header, err := tar.FileInfoHeader(stat, stat.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
header.Name = filePath
|
|
err = tarWriter.WriteHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(tarWriter, file)
|
|
return err
|
|
}
|
|
|
|
func debugAnalyze(ctx *sql.Context, tempDir string, sqlEng *engine.SqlEngine, sqlFile *os.File) error {
|
|
_, err := sqlFile.Seek(0, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
sqlFile.Seek(0, 0)
|
|
}()
|
|
|
|
eng := sqlEng.GetUnderlyingEngine()
|
|
eng.Analyzer.Debug = true
|
|
eng.Analyzer.Verbose = true
|
|
defer func() {
|
|
eng.Analyzer.Debug = false
|
|
eng.Analyzer.Verbose = false
|
|
}()
|
|
analysisFile, err := os.Create(filepath.Join(tempDir, "analysis.txt"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer analysisFile.Close()
|
|
|
|
planFile, err := os.Create(filepath.Join(tempDir, "plan.txt"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer planFile.Close()
|
|
planBuf := bufio.NewWriter(planFile)
|
|
|
|
analyzer.SetOutput(analysisFile)
|
|
logrus.SetOutput(analysisFile)
|
|
log.SetOutput(analysisFile)
|
|
origStdout := os.Stdout
|
|
origStderr := os.Stderr
|
|
origCliErr := cli.CliErr
|
|
origCliOut := cli.CliOut
|
|
os.Stdout = analysisFile
|
|
os.Stderr = analysisFile
|
|
cli.CliErr = analysisFile
|
|
cli.CliOut = analysisFile
|
|
defer func() {
|
|
logrus.SetOutput(os.Stderr)
|
|
analyzer.SetOutput(os.Stderr)
|
|
log.SetOutput(os.Stderr)
|
|
os.Stdout = origStdout
|
|
os.Stderr = origStderr
|
|
cli.CliOut = origCliOut
|
|
cli.CliErr = origCliErr
|
|
}()
|
|
|
|
scanner := NewStreamScanner(sqlFile)
|
|
var query string
|
|
for scanner.Scan() {
|
|
if fileReadProg != nil {
|
|
updateFileReadProgressOutput()
|
|
fileReadProg.setReadBytes(int64(len(scanner.Bytes())))
|
|
}
|
|
query += scanner.Text()
|
|
if len(query) == 0 || query == "\n" {
|
|
continue
|
|
}
|
|
|
|
cli.Println("analyzing: " + query[:min(len(query), 60)])
|
|
|
|
planned, err := eng.AnalyzeQuery(ctx, query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(planBuf, "query: %s\n", query)
|
|
fmt.Fprintf(planBuf, "plan: \n%s", planned.String())
|
|
fmt.Fprintf(planBuf, "debug plan: \n%s", sql.DebugString(planned))
|
|
}
|
|
|
|
return planBuf.Flush()
|
|
}
|
|
|
|
func execDebugMode(ctx *sql.Context, qryist cli.Queryist, queryFile *os.File, continueOnErr bool, format engine.PrintResultFormat) error {
|
|
_, err := queryFile.Seek(0, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
queryFile.Seek(0, 0)
|
|
}()
|
|
input := bufio.NewReader(transform.NewReader(queryFile, textunicode.BOMOverride(transform.Nop)))
|
|
|
|
return execBatchMode(ctx, qryist, input, continueOnErr, format, false)
|
|
}
|