Skip to content

Commit e7abfe7

Browse files
feat: implement :perftrace and :help interactive commands
:perftrace redirects timing/statistics output to a file, stderr, or stdout. :help displays the list of available sqlcmd commands with usage. Both commands validate arguments and return appropriate errors.
1 parent 56b1fb1 commit e7abfe7

File tree

5 files changed

+171
-0
lines changed

5 files changed

+171
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ switches are most important to you to have implemented next in the new sqlcmd.
154154
- `:Connect` now has an optional `-G` parameter to select one of the authentication methods for Azure SQL Database - `SqlAuthentication`, `ActiveDirectoryDefault`, `ActiveDirectoryIntegrated`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, `ActiveDirectoryPassword`. If `-G` is not provided, either Integrated security or SQL Authentication will be used, dependent on the presence of a `-U` username parameter.
155155
- The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces.
156156
- Sqlcmd can now print results using a vertical format. Use the new `--vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable.
157+
- `:help` displays a list of available sqlcmd commands.
157158

158159
```
159160
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid
@@ -163,6 +164,14 @@ client_interface_name go-mssqldb
163164
program_name sqlcmd
164165
```
165166

167+
- `:perftrace` redirects performance statistics output to a file, stderr, or stdout. Use in conjunction with `-p` flag.
168+
169+
```
170+
1> :perftrace c:/logs/perf.txt
171+
1> select 1
172+
2> go
173+
```
174+
166175
- `sqlcmd` supports shared memory and named pipe transport. Use the appropriate protocol prefix on the server name to force a protocol:
167176
* `lpc` for shared memory, only for a localhost. `sqlcmd -S lpc:.`
168177
* `np` for named pipes. Or use the UNC named pipe path as the server name: `sqlcmd -S \\myserver\pipe\sql\query`

cmd/sqlcmd/sqlcmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
909909
}
910910
s.SetOutput(nil)
911911
s.SetError(nil)
912+
s.SetStat(nil)
912913
return s.Exitcode, err
913914
}
914915

pkg/sqlcmd/commands.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ func newCommands() Commands {
113113
action: xmlCommand,
114114
name: "XML",
115115
},
116+
"HELP": {
117+
regex: regexp.MustCompile(`(?im)^[ \t]*:HELP(?:[ \t]+(.*$)|$)`),
118+
action: helpCommand,
119+
name: "HELP",
120+
},
121+
"PERFTRACE": {
122+
regex: regexp.MustCompile(`(?im)^[ \t]*:PERFTRACE(?:[ \t]+(.*$)|$)`),
123+
action: perftraceCommand,
124+
name: "PERFTRACE",
125+
},
116126
}
117127
}
118128

@@ -596,6 +606,77 @@ func xmlCommand(s *Sqlcmd, args []string, line uint) error {
596606
return nil
597607
}
598608

609+
// helpCommand displays the list of available sqlcmd commands
610+
func helpCommand(s *Sqlcmd, args []string, line uint) error {
611+
helpText := `:!! [<command>]
612+
- Executes a command in the operating system shell.
613+
:connect server[\instance] [-l timeout] [-U user [-P password]]
614+
- Connects to a SQL Server instance.
615+
:ed
616+
- Edits the current or last executed statement cache.
617+
:error <dest>
618+
- Redirects error output to a file, stderr, or stdout.
619+
:exit
620+
- Quits sqlcmd immediately.
621+
:exit()
622+
- Execute statement cache; quit with no return value.
623+
:exit(<query>)
624+
- Execute the specified query; returns numeric result.
625+
go [<n>]
626+
- Executes the statement cache (n times).
627+
:help
628+
- Shows this list of commands.
629+
:list
630+
- Prints the content of the statement cache.
631+
:listvar
632+
- Lists the set sqlcmd scripting variables.
633+
:on error [exit|ignore]
634+
- Action for batch or sqlcmd command errors.
635+
:out <filename>|stderr|stdout
636+
- Redirects query output to a file, stderr, or stdout.
637+
:perftrace <filename>|stderr|stdout
638+
- Redirects timing output to a file, stderr, or stdout.
639+
:quit
640+
- Quits sqlcmd immediately.
641+
:r <filename>
642+
- Append file contents to the statement cache.
643+
:reset
644+
- Discards the statement cache.
645+
:setvar {variable}
646+
- Removes a sqlcmd scripting variable.
647+
:setvar <variable> <value>
648+
- Sets a sqlcmd scripting variable.
649+
:xml [on|off]
650+
- Sets XML output mode.
651+
`
652+
_, err := s.GetOutput().Write([]byte(helpText))
653+
return err
654+
}
655+
656+
// perftraceCommand changes the performance statistics writer to use a file
657+
func perftraceCommand(s *Sqlcmd, args []string, line uint) error {
658+
if len(args) == 0 || args[0] == "" {
659+
return InvalidCommandError("PERFTRACE", line)
660+
}
661+
filePath, err := resolveArgumentVariables(s, []rune(args[0]), true)
662+
if err != nil {
663+
return err
664+
}
665+
switch {
666+
case strings.EqualFold(filePath, "stderr"):
667+
s.SetStat(os.Stderr)
668+
case strings.EqualFold(filePath, "stdout"):
669+
s.SetStat(os.Stdout)
670+
default:
671+
o, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
672+
if err != nil {
673+
return InvalidFileError(err, args[0])
674+
}
675+
s.SetStat(o)
676+
}
677+
return nil
678+
}
679+
599680
func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) {
600681
var b *strings.Builder
601682
end := len(arg)

pkg/sqlcmd/commands_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func TestCommandParsing(t *testing.T) {
5454
{`:XML ON `, "XML", []string{`ON `}},
5555
{`:RESET`, "RESET", []string{""}},
5656
{`RESET`, "RESET", []string{""}},
57+
{`:HELP`, "HELP", []string{""}},
58+
{`:help`, "HELP", []string{""}},
59+
{`:PERFTRACE stderr`, "PERFTRACE", []string{"stderr"}},
60+
{`:perftrace c:/logs/perf.txt`, "PERFTRACE", []string{"c:/logs/perf.txt"}},
5761
}
5862

5963
for _, test := range commands {
@@ -458,3 +462,62 @@ func TestExitCommandAppendsParameterToCurrentBatch(t *testing.T) {
458462
}
459463

460464
}
465+
466+
func TestHelpCommand(t *testing.T) {
467+
s, buf := setupSqlCmdWithMemoryOutput(t)
468+
defer buf.Close()
469+
s.SetOutput(buf)
470+
471+
err := helpCommand(s, []string{""}, 1)
472+
assert.NoError(t, err, "helpCommand should not error")
473+
474+
output := buf.buf.String()
475+
// Verify key commands are listed
476+
assert.Contains(t, output, ":connect", "help should list :connect")
477+
assert.Contains(t, output, ":exit", "help should list :exit")
478+
assert.Contains(t, output, ":help", "help should list :help")
479+
assert.Contains(t, output, ":setvar", "help should list :setvar")
480+
assert.Contains(t, output, ":listvar", "help should list :listvar")
481+
assert.Contains(t, output, ":out", "help should list :out")
482+
assert.Contains(t, output, ":error", "help should list :error")
483+
assert.Contains(t, output, ":perftrace", "help should list :perftrace")
484+
assert.Contains(t, output, ":r", "help should list :r")
485+
assert.Contains(t, output, "go", "help should list go")
486+
}
487+
488+
func TestPerftraceCommand(t *testing.T) {
489+
s, buf := setupSqlCmdWithMemoryOutput(t)
490+
defer buf.Close()
491+
492+
// Test empty argument returns error
493+
err := perftraceCommand(s, []string{""}, 1)
494+
assert.EqualError(t, err, InvalidCommandError("PERFTRACE", 1).Error(), "perftraceCommand with empty argument")
495+
496+
// Test redirect to stdout
497+
err = perftraceCommand(s, []string{"stdout"}, 1)
498+
assert.NoError(t, err, "perftraceCommand with stdout")
499+
assert.Equal(t, os.Stdout, s.GetStat(), "stat set to stdout")
500+
501+
// Test redirect to stderr
502+
err = perftraceCommand(s, []string{"stderr"}, 1)
503+
assert.NoError(t, err, "perftraceCommand with stderr")
504+
assert.Equal(t, os.Stderr, s.GetStat(), "stat set to stderr")
505+
506+
// Test redirect to file
507+
file, err := os.CreateTemp("", "sqlcmdperf")
508+
assert.NoError(t, err, "os.CreateTemp")
509+
defer os.Remove(file.Name())
510+
fileName := file.Name()
511+
_ = file.Close()
512+
513+
err = perftraceCommand(s, []string{fileName}, 1)
514+
assert.NoError(t, err, "perftraceCommand with file path")
515+
// Clean up by setting stat to nil
516+
s.SetStat(nil)
517+
518+
// Test variable resolution
519+
s.vars.Set("myvar", "stdout")
520+
err = perftraceCommand(s, []string{"$(myvar)"}, 1)
521+
assert.NoError(t, err, "perftraceCommand with a variable")
522+
assert.Equal(t, os.Stdout, s.GetStat(), "stat set to stdout using a variable")
523+
}

pkg/sqlcmd/sqlcmd.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type Sqlcmd struct {
6767
db *sql.Conn
6868
out io.WriteCloser
6969
err io.WriteCloser
70+
stat io.WriteCloser
7071
batch *Batch
7172
echoFileLines bool
7273
// Exitcode is returned to the operating system when the process exits
@@ -236,6 +237,22 @@ func (s *Sqlcmd) SetError(e io.WriteCloser) {
236237
s.err = e
237238
}
238239

240+
// GetStat returns the io.Writer to use for performance statistics
241+
func (s *Sqlcmd) GetStat() io.Writer {
242+
if s.stat == nil {
243+
return s.GetOutput()
244+
}
245+
return s.stat
246+
}
247+
248+
// SetStat sets the io.WriteCloser to use for performance statistics
249+
func (s *Sqlcmd) SetStat(st io.WriteCloser) {
250+
if s.stat != nil && s.stat != os.Stderr && s.stat != os.Stdout {
251+
s.stat.Close()
252+
}
253+
s.stat = st
254+
}
255+
239256
// WriteError writes the error on specified stream
240257
func (s *Sqlcmd) WriteError(stream io.Writer, err error) {
241258
if serr, ok := err.(SqlcmdError); ok {

0 commit comments

Comments
 (0)