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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ 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.
- `-p` prints performance statistics after each batch execution. Use `-p` for standard format or `-p1` for colon-separated format suitable for parsing.

```
1> select 1
2> go

Network packet size (bytes): 4096
1 xact[s]:
Clock Time (ms.): total 5 avg 5.00 (200.00 xacts per sec.)
```

```
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid
Expand Down
14 changes: 14 additions & 0 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type SQLCmdArguments struct {
ChangePassword string
ChangePasswordAndExit string
TraceFile string
PrintStatistics *int
// Keep Help at the end of the list
Help bool
}
Expand Down Expand Up @@ -126,6 +127,7 @@ const (
disableCmdAndWarn = "disable-cmd-and-warn"
listServers = "list-servers"
removeControlCharacters = "remove-control-characters"
printStatistics = "print-statistics"
)

func encryptConnectionAllowsTLS(value string) bool {
Expand Down Expand Up @@ -330,6 +332,7 @@ func checkDefaultValue(args []string, i int) (val string) {
'k': "0",
'L': "|", // | is the sentinel for no value since users are unlikely to use it. It's "reserved" in most shells
'X': "0",
'p': "0",
}
if isFlag(args[i]) && len(args[i]) == 2 && (len(args) == i+1 || args[i+1][0] == '-') {
if v, ok := flags[rune(args[i][1])]; ok {
Expand Down Expand Up @@ -393,6 +396,7 @@ func SetScreenWidthFlags(args *SQLCmdArguments, rootCmd *cobra.Command) {
args.DisableCmd = getOptionalIntArgument(rootCmd, disableCmdAndWarn)
args.ErrorsToStderr = getOptionalIntArgument(rootCmd, errorsToStderr)
args.RemoveControlCharacters = getOptionalIntArgument(rootCmd, removeControlCharacters)
args.PrintStatistics = getOptionalIntArgument(rootCmd, printStatistics)
}

func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
Expand Down Expand Up @@ -475,6 +479,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
_ = rootCmd.Flags().IntP(removeControlCharacters, "k", 0, localizer.Sprintf("%s Remove control characters from output. Pass 1 to substitute a space per character, 2 for a space per consecutive characters", "-k [1|2]"))
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
_ = rootCmd.Flags().IntP(printStatistics, "p", 0, localizer.Sprintf("%s Print performance statistics after each batch. Pass 1 for colon-separated format", "-p[1]"))
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
Expand Down Expand Up @@ -543,6 +548,14 @@ func normalizeFlags(cmd *cobra.Command) error {
err = invalidParameterError("-k", v, "1", "2")
return pflag.NormalizedName("")
}
case printStatistics:
switch v {
case "0", "1":
return pflag.NormalizedName(name)
default:
err = invalidParameterError("-p", v, "0", "1")
return pflag.NormalizedName("")
}
}

return pflag.NormalizedName(name)
Expand Down Expand Up @@ -812,6 +825,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
s.SetupCloseHandler()
defer s.StopCloseHandler()
s.UnicodeOutputFile = args.UnicodeOutputFile
s.PrintStatistics = args.PrintStatistics

if args.DisableCmd != nil {
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())
Expand Down
6 changes: 6 additions & 0 deletions cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-k", "-X", "-r", "-z", "something"}, func(args SQLCmdArguments) bool {
return args.warnOnBlockedCmd() && !args.useEnvVars() && args.getControlCharacterBehavior() == sqlcmd.ControlRemove && *args.ErrorsToStderr == 0 && args.ChangePassword == "something"
}},
{[]string{"-p"}, func(args SQLCmdArguments) bool {
return args.PrintStatistics != nil && *args.PrintStatistics == 0
}},
{[]string{"-p", "1"}, func(args SQLCmdArguments) bool {
return args.PrintStatistics != nil && *args.PrintStatistics == 1
}},
{[]string{"-N"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "true"
}},
Expand Down
44 changes: 42 additions & 2 deletions pkg/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ type Sqlcmd struct {
UnicodeOutputFile bool
// EchoInput tells the GO command to print the batch text before running the query
EchoInput bool
colorizer color.Colorizer
termchan chan os.Signal
// PrintStatistics controls printing of performance statistics after each batch
// nil means disabled, 0 means standard format, 1 means colon-separated format
PrintStatistics *int
colorizer color.Colorizer
termchan chan os.Signal
}

// New creates a new Sqlcmd instance.
Expand Down Expand Up @@ -421,6 +424,7 @@ func (s *Sqlcmd) getRunnableQuery(q string) string {
// -102: Conversion error occurred when selecting return value
func (s *Sqlcmd) runQuery(query string) (int, error) {
retcode := -101
startTime := time.Now()
s.Format.BeginBatch(query, s.vars, s.GetOutput(), s.GetError())
ctx := context.Background()
timeout := s.vars.QueryTimeoutSeconds()
Expand Down Expand Up @@ -508,6 +512,8 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
}
}
s.Format.EndBatch()
elapsedMs := time.Since(startTime).Milliseconds()
s.printStatistics(elapsedMs, 1)
return retcode, qe
}

Expand Down Expand Up @@ -580,3 +586,37 @@ func (s *Sqlcmd) SetupCloseHandler() {
func (s *Sqlcmd) StopCloseHandler() {
signal.Stop(s.termchan)
}

// printStatistics prints performance statistics after a batch execution
// if PrintStatistics is enabled
func (s *Sqlcmd) printStatistics(elapsedMs int64, numBatches int) {
if s.PrintStatistics == nil || numBatches <= 0 {
return
}

// Get packet size from connect settings or use default
packetSize := s.Connect.PacketSize
if packetSize <= 0 {
packetSize = 4096 // default packet size
}

// Ensure minimum 1ms for calculations
if elapsedMs < 1 {
elapsedMs = 1
}

avgTime := float64(elapsedMs) / float64(numBatches)
batchesPerSec := float64(numBatches) / (float64(elapsedMs) / 1000.0)

out := s.GetOutput()
if *s.PrintStatistics == 1 {
// Colon-separated format: n:x:t1:t2:t3
// packetSize:numBatches:totalTime:avgTime:batchesPerSec
fmt.Fprintf(out, "\n%d:%d:%d:%.2f:%.2f\n", packetSize, numBatches, elapsedMs, avgTime, batchesPerSec)
} else {
// Standard format
fmt.Fprintf(out, "\nNetwork packet size (bytes): %d\n", packetSize)
fmt.Fprintf(out, "%d xact[s]:\n", numBatches)
fmt.Fprintf(out, "Clock Time (ms.): total %7d avg %.2f (%.2f xacts per sec.)\n", elapsedMs, avgTime, batchesPerSec)
}
}
42 changes: 42 additions & 0 deletions pkg/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,45 @@ func TestSqlcmdPrefersSharedMemoryProtocol(t *testing.T) {
assert.EqualValuesf(t, "np", msdsn.ProtocolParsers[3].Protocol(), "np should be fourth protocol")

}

func TestPrintStatisticsStandardFormat(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
standardFormat := 0
s.PrintStatistics = &standardFormat
s.Connect.PacketSize = 4096
_, err := s.runQuery("SELECT 1")
assert.NoError(t, err, "runQuery failed")
output := buf.buf.String()
// Standard format should contain specific phrases
assert.Contains(t, output, "Network packet size (bytes): 4096", "Should contain packet size")
assert.Contains(t, output, "xact[s]:", "Should contain xacts label")
assert.Contains(t, output, "Clock Time (ms.):", "Should contain clock time label")
assert.Contains(t, output, "xacts per sec.", "Should contain xacts per sec")
}

func TestPrintStatisticsColonFormat(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
colonFormat := 1
s.PrintStatistics = &colonFormat
s.Connect.PacketSize = 8192
_, err := s.runQuery("SELECT 1")
assert.NoError(t, err, "runQuery failed")
output := buf.buf.String()
// Colon format: packetSize:numBatches:totalTime:avgTime:batchesPerSec
// Should start with 8192:1:
assert.Contains(t, output, "8192:1:", "Should contain packet size and batch count in colon format")
}

func TestPrintStatisticsDisabled(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
// PrintStatistics is nil by default (disabled)
_, err := s.runQuery("SELECT 1")
assert.NoError(t, err, "runQuery failed")
output := buf.buf.String()
// Should not contain statistics output
assert.NotContains(t, output, "Network packet size", "Should not contain packet size when disabled")
assert.NotContains(t, output, "xact[s]:", "Should not contain xacts label when disabled")
}
Loading