Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ switches are most important to you to have implemented next in the new sqlcmd.
- `: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.
- 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.
- 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.
- `:help` displays a list of available sqlcmd commands.

```
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid
Expand All @@ -163,6 +164,14 @@ client_interface_name go-mssqldb
program_name sqlcmd
```

- `:perftrace` redirects performance statistics output to a file, stderr, or stdout. Use in conjunction with `-p` flag.

```
1> :perftrace c:/logs/perf.txt
1> select 1
2> go
```

- `sqlcmd` supports shared memory and named pipe transport. Use the appropriate protocol prefix on the server name to force a protocol:
* `lpc` for shared memory, only for a localhost. `sqlcmd -S lpc:.`
* `np` for named pipes. Or use the UNC named pipe path as the server name: `sqlcmd -S \\myserver\pipe\sql\query`
Expand Down
1 change: 1 addition & 0 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
}
s.SetOutput(nil)
s.SetError(nil)
s.SetStat(nil)
return s.Exitcode, err
}

Expand Down
81 changes: 81 additions & 0 deletions pkg/sqlcmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ func newCommands() Commands {
action: xmlCommand,
name: "XML",
},
"HELP": {
regex: regexp.MustCompile(`(?im)^[ \t]*:HELP(?:[ \t]+(.*$)|$)`),
action: helpCommand,
name: "HELP",
},
"PERFTRACE": {
regex: regexp.MustCompile(`(?im)^[ \t]*:PERFTRACE(?:[ \t]+(.*$)|$)`),
action: perftraceCommand,
name: "PERFTRACE",
},
}
}

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

// helpCommand displays the list of available sqlcmd commands
func helpCommand(s *Sqlcmd, args []string, line uint) error {
helpText := `:!! [<command>]
- Executes a command in the operating system shell.
:connect server[\instance] [-l timeout] [-U user [-P password]]
- Connects to a SQL Server instance.
:ed
- Edits the current or last executed statement cache.
:error <dest>
- Redirects error output to a file, stderr, or stdout.
:exit
- Quits sqlcmd immediately.
:exit()
- Execute statement cache; quit with no return value.
:exit(<query>)
- Execute the specified query; returns numeric result.
go [<n>]
- Executes the statement cache (n times).
:help
- Shows this list of commands.
:list
- Prints the content of the statement cache.
:listvar
- Lists the set sqlcmd scripting variables.
:on error [exit|ignore]
- Action for batch or sqlcmd command errors.
:out <filename>|stderr|stdout
- Redirects query output to a file, stderr, or stdout.
:perftrace <filename>|stderr|stdout
- Redirects timing output to a file, stderr, or stdout.
:quit
- Quits sqlcmd immediately.
:r <filename>
- Append file contents to the statement cache.
:reset
- Discards the statement cache.
:setvar {variable}
- Removes a sqlcmd scripting variable.
:setvar <variable> <value>
- Sets a sqlcmd scripting variable.
:xml [on|off]
- Sets XML output mode.
`
_, err := s.GetOutput().Write([]byte(helpText))
return err
}

// perftraceCommand changes the performance statistics writer to use a file
func perftraceCommand(s *Sqlcmd, args []string, line uint) error {
if len(args) == 0 || args[0] == "" {
return InvalidCommandError("PERFTRACE", line)
}
filePath, err := resolveArgumentVariables(s, []rune(args[0]), true)
if err != nil {
return err
}
switch {
case strings.EqualFold(filePath, "stderr"):
s.SetStat(os.Stderr)
case strings.EqualFold(filePath, "stdout"):
s.SetStat(os.Stdout)
default:
o, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return InvalidFileError(err, args[0])
}
s.SetStat(o)
}
return nil
}

func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) {
var b *strings.Builder
end := len(arg)
Expand Down
63 changes: 63 additions & 0 deletions pkg/sqlcmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func TestCommandParsing(t *testing.T) {
{`:XML ON `, "XML", []string{`ON `}},
{`:RESET`, "RESET", []string{""}},
{`RESET`, "RESET", []string{""}},
{`:HELP`, "HELP", []string{""}},
{`:help`, "HELP", []string{""}},
{`:PERFTRACE stderr`, "PERFTRACE", []string{"stderr"}},
{`:perftrace c:/logs/perf.txt`, "PERFTRACE", []string{"c:/logs/perf.txt"}},
}

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

}

func TestHelpCommand(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.SetOutput(buf)

err := helpCommand(s, []string{""}, 1)
assert.NoError(t, err, "helpCommand should not error")

output := buf.buf.String()
// Verify key commands are listed
assert.Contains(t, output, ":connect", "help should list :connect")
assert.Contains(t, output, ":exit", "help should list :exit")
assert.Contains(t, output, ":help", "help should list :help")
assert.Contains(t, output, ":setvar", "help should list :setvar")
assert.Contains(t, output, ":listvar", "help should list :listvar")
assert.Contains(t, output, ":out", "help should list :out")
assert.Contains(t, output, ":error", "help should list :error")
assert.Contains(t, output, ":perftrace", "help should list :perftrace")
assert.Contains(t, output, ":r", "help should list :r")
assert.Contains(t, output, "go", "help should list go")
}

func TestPerftraceCommand(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()

// Test empty argument returns error
err := perftraceCommand(s, []string{""}, 1)
assert.EqualError(t, err, InvalidCommandError("PERFTRACE", 1).Error(), "perftraceCommand with empty argument")

// Test redirect to stdout
err = perftraceCommand(s, []string{"stdout"}, 1)
assert.NoError(t, err, "perftraceCommand with stdout")
assert.Equal(t, os.Stdout, s.GetStat(), "stat set to stdout")

// Test redirect to stderr
err = perftraceCommand(s, []string{"stderr"}, 1)
assert.NoError(t, err, "perftraceCommand with stderr")
assert.Equal(t, os.Stderr, s.GetStat(), "stat set to stderr")

// Test redirect to file
file, err := os.CreateTemp("", "sqlcmdperf")
assert.NoError(t, err, "os.CreateTemp")
defer os.Remove(file.Name())
fileName := file.Name()
_ = file.Close()

err = perftraceCommand(s, []string{fileName}, 1)
assert.NoError(t, err, "perftraceCommand with file path")
// Clean up by setting stat to nil
s.SetStat(nil)

// Test variable resolution
s.vars.Set("myvar", "stdout")
err = perftraceCommand(s, []string{"$(myvar)"}, 1)
assert.NoError(t, err, "perftraceCommand with a variable")
assert.Equal(t, os.Stdout, s.GetStat(), "stat set to stdout using a variable")
}
17 changes: 17 additions & 0 deletions pkg/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Sqlcmd struct {
db *sql.Conn
out io.WriteCloser
err io.WriteCloser
stat io.WriteCloser
batch *Batch
echoFileLines bool
// Exitcode is returned to the operating system when the process exits
Expand Down Expand Up @@ -236,6 +237,22 @@ func (s *Sqlcmd) SetError(e io.WriteCloser) {
s.err = e
}

// GetStat returns the io.Writer to use for performance statistics
func (s *Sqlcmd) GetStat() io.Writer {
if s.stat == nil {
return s.GetOutput()
}
return s.stat
}

// SetStat sets the io.WriteCloser to use for performance statistics
func (s *Sqlcmd) SetStat(st io.WriteCloser) {
if s.stat != nil && s.stat != os.Stderr && s.stat != os.Stdout {
s.stat.Close()
}
s.stat = st
}

// WriteError writes the error on specified stream
func (s *Sqlcmd) WriteError(stream io.Writer, err error) {
if serr, ok := err.(SqlcmdError); ok {
Expand Down
Loading