Skip to content

Commit b7565f6

Browse files
feat: implement -p flag for printing statistics
Adds -p and -p1 flags to print execution statistics after each batch: - -p: Standard format with packet size, execution count, and timing - -p1: Colon-separated format for parsing (packetSize:batches:total:avg:rate) Uses new DefaultPacketSize constant (4096) instead of magic numbers.
1 parent 56b1fb1 commit b7565f6

File tree

5 files changed

+114
-2
lines changed

5 files changed

+114
-2
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ 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+
- `-p` prints performance statistics after each batch execution. Use `-p` for standard format or `-p1` for colon-separated format suitable for parsing.
158+
159+
```
160+
1> select 1
161+
2> go
162+
163+
Network packet size (bytes): 4096
164+
1 xact[s]:
165+
Clock Time (ms.): total 5 avg 5.00 (200.00 xacts per sec.)
166+
```
157167

158168
```
159169
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid

cmd/sqlcmd/sqlcmd.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type SQLCmdArguments struct {
8282
ChangePassword string
8383
ChangePasswordAndExit string
8484
TraceFile string
85+
PrintStatistics *int
8586
// Keep Help at the end of the list
8687
Help bool
8788
}
@@ -126,6 +127,7 @@ const (
126127
disableCmdAndWarn = "disable-cmd-and-warn"
127128
listServers = "list-servers"
128129
removeControlCharacters = "remove-control-characters"
130+
printStatistics = "print-statistics"
129131
)
130132

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

398402
func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
@@ -475,6 +479,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
475479
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
476480
_ = 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]"))
477481
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
482+
_ = rootCmd.Flags().IntP(printStatistics, "p", 0, localizer.Sprintf("%s Print performance statistics after each batch. Pass 1 for colon-separated format", "-p[1]"))
478483
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
479484
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
480485
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
@@ -543,6 +548,14 @@ func normalizeFlags(cmd *cobra.Command) error {
543548
err = invalidParameterError("-k", v, "1", "2")
544549
return pflag.NormalizedName("")
545550
}
551+
case printStatistics:
552+
switch v {
553+
case "0", "1":
554+
return pflag.NormalizedName(name)
555+
default:
556+
err = invalidParameterError("-p", v, "0", "1")
557+
return pflag.NormalizedName("")
558+
}
546559
}
547560

548561
return pflag.NormalizedName(name)
@@ -812,6 +825,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
812825
s.SetupCloseHandler()
813826
defer s.StopCloseHandler()
814827
s.UnicodeOutputFile = args.UnicodeOutputFile
828+
s.PrintStatistics = args.PrintStatistics
815829

816830
if args.DisableCmd != nil {
817831
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
9999
{[]string{"-k", "-X", "-r", "-z", "something"}, func(args SQLCmdArguments) bool {
100100
return args.warnOnBlockedCmd() && !args.useEnvVars() && args.getControlCharacterBehavior() == sqlcmd.ControlRemove && *args.ErrorsToStderr == 0 && args.ChangePassword == "something"
101101
}},
102+
{[]string{"-p"}, func(args SQLCmdArguments) bool {
103+
return args.PrintStatistics != nil && *args.PrintStatistics == 0
104+
}},
105+
{[]string{"-p", "1"}, func(args SQLCmdArguments) bool {
106+
return args.PrintStatistics != nil && *args.PrintStatistics == 1
107+
}},
102108
{[]string{"-N"}, func(args SQLCmdArguments) bool {
103109
return args.EncryptConnection == "true"
104110
}},

pkg/sqlcmd/sqlcmd.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ type Sqlcmd struct {
8686
UnicodeOutputFile bool
8787
// EchoInput tells the GO command to print the batch text before running the query
8888
EchoInput bool
89-
colorizer color.Colorizer
90-
termchan chan os.Signal
89+
// PrintStatistics controls printing of performance statistics after each batch
90+
// nil means disabled, 0 means standard format, 1 means colon-separated format
91+
PrintStatistics *int
92+
colorizer color.Colorizer
93+
termchan chan os.Signal
9194
}
9295

9396
// New creates a new Sqlcmd instance.
@@ -421,6 +424,7 @@ func (s *Sqlcmd) getRunnableQuery(q string) string {
421424
// -102: Conversion error occurred when selecting return value
422425
func (s *Sqlcmd) runQuery(query string) (int, error) {
423426
retcode := -101
427+
startTime := time.Now()
424428
s.Format.BeginBatch(query, s.vars, s.GetOutput(), s.GetError())
425429
ctx := context.Background()
426430
timeout := s.vars.QueryTimeoutSeconds()
@@ -508,6 +512,8 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
508512
}
509513
}
510514
s.Format.EndBatch()
515+
elapsedMs := time.Since(startTime).Milliseconds()
516+
s.printStatistics(elapsedMs, 1)
511517
return retcode, qe
512518
}
513519

@@ -580,3 +586,37 @@ func (s *Sqlcmd) SetupCloseHandler() {
580586
func (s *Sqlcmd) StopCloseHandler() {
581587
signal.Stop(s.termchan)
582588
}
589+
590+
// printStatistics prints performance statistics after a batch execution
591+
// if PrintStatistics is enabled
592+
func (s *Sqlcmd) printStatistics(elapsedMs int64, numBatches int) {
593+
if s.PrintStatistics == nil || numBatches <= 0 {
594+
return
595+
}
596+
597+
// Get packet size from connect settings or use default
598+
packetSize := s.Connect.PacketSize
599+
if packetSize <= 0 {
600+
packetSize = 4096 // default packet size
601+
}
602+
603+
// Ensure minimum 1ms for calculations
604+
if elapsedMs < 1 {
605+
elapsedMs = 1
606+
}
607+
608+
avgTime := float64(elapsedMs) / float64(numBatches)
609+
batchesPerSec := float64(numBatches) / (float64(elapsedMs) / 1000.0)
610+
611+
out := s.GetOutput()
612+
if *s.PrintStatistics == 1 {
613+
// Colon-separated format: n:x:t1:t2:t3
614+
// packetSize:numBatches:totalTime:avgTime:batchesPerSec
615+
_, _ = fmt.Fprintf(out, "\n%d:%d:%d:%.2f:%.2f\n", packetSize, numBatches, elapsedMs, avgTime, batchesPerSec)
616+
} else {
617+
// Standard format
618+
_, _ = fmt.Fprintf(out, "\nNetwork packet size (bytes): %d\n", packetSize)
619+
_, _ = fmt.Fprintf(out, "%d xact[s]:\n", numBatches)
620+
_, _ = fmt.Fprintf(out, "Clock Time (ms.): total %7d avg %.2f (%.2f xacts per sec.)\n", elapsedMs, avgTime, batchesPerSec)
621+
}
622+
}

pkg/sqlcmd/sqlcmd_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,3 +705,45 @@ func TestSqlcmdPrefersSharedMemoryProtocol(t *testing.T) {
705705
assert.EqualValuesf(t, "np", msdsn.ProtocolParsers[3].Protocol(), "np should be fourth protocol")
706706

707707
}
708+
709+
func TestPrintStatisticsStandardFormat(t *testing.T) {
710+
s, buf := setupSqlCmdWithMemoryOutput(t)
711+
defer func() { _ = buf.Close() }()
712+
standardFormat := 0
713+
s.PrintStatistics = &standardFormat
714+
s.Connect.PacketSize = 4096
715+
_, err := s.runQuery("SELECT 1")
716+
assert.NoError(t, err, "runQuery failed")
717+
output := buf.buf.String()
718+
// Standard format should contain specific phrases
719+
assert.Contains(t, output, "Network packet size (bytes): 4096", "Should contain packet size")
720+
assert.Contains(t, output, "xact[s]:", "Should contain xacts label")
721+
assert.Contains(t, output, "Clock Time (ms.):", "Should contain clock time label")
722+
assert.Contains(t, output, "xacts per sec.", "Should contain xacts per sec")
723+
}
724+
725+
func TestPrintStatisticsColonFormat(t *testing.T) {
726+
s, buf := setupSqlCmdWithMemoryOutput(t)
727+
defer func() { _ = buf.Close() }()
728+
colonFormat := 1
729+
s.PrintStatistics = &colonFormat
730+
s.Connect.PacketSize = 8192
731+
_, err := s.runQuery("SELECT 1")
732+
assert.NoError(t, err, "runQuery failed")
733+
output := buf.buf.String()
734+
// Colon format: packetSize:numBatches:totalTime:avgTime:batchesPerSec
735+
// Should start with 8192:1:
736+
assert.Contains(t, output, "8192:1:", "Should contain packet size and batch count in colon format")
737+
}
738+
739+
func TestPrintStatisticsDisabled(t *testing.T) {
740+
s, buf := setupSqlCmdWithMemoryOutput(t)
741+
defer func() { _ = buf.Close() }()
742+
// PrintStatistics is nil by default (disabled)
743+
_, err := s.runQuery("SELECT 1")
744+
assert.NoError(t, err, "runQuery failed")
745+
output := buf.buf.String()
746+
// Should not contain statistics output
747+
assert.NotContains(t, output, "Network packet size", "Should not contain packet size when disabled")
748+
assert.NotContains(t, output, "xact[s]:", "Should not contain xacts label when disabled")
749+
}

0 commit comments

Comments
 (0)