Skip to content

Commit 20182ab

Browse files
Implement -p flag for print statistics
Add support for the -p flag which prints performance statistics after each batch execution: - -p (or -p0): Standard human-readable format - -p1: Colon-separated format for parsing Statistics include network packet size, transaction count, and timing information (total ms, average ms, transactions per second). This improves compatibility with legacy ODBC sqlcmd.
1 parent 758fca9 commit 20182ab

File tree

4 files changed

+107
-2
lines changed

4 files changed

+107
-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: 13 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 {
@@ -393,6 +395,7 @@ func SetScreenWidthFlags(args *SQLCmdArguments, rootCmd *cobra.Command) {
393395
args.DisableCmd = getOptionalIntArgument(rootCmd, disableCmdAndWarn)
394396
args.ErrorsToStderr = getOptionalIntArgument(rootCmd, errorsToStderr)
395397
args.RemoveControlCharacters = getOptionalIntArgument(rootCmd, removeControlCharacters)
398+
args.PrintStatistics = getOptionalIntArgument(rootCmd, printStatistics)
396399
}
397400

398401
func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
@@ -475,6 +478,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
475478
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
476479
_ = 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]"))
477480
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
481+
_ = rootCmd.Flags().IntP(printStatistics, "p", 0, localizer.Sprintf("%s Print performance statistics after each batch. Pass 1 for colon-separated format", "-p[1]"))
478482
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
479483
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
480484
rootCmd.Flags().StringVarP(&args.ChangePassword, "change-password", "z", "", localizer.Sprintf("New password"))
@@ -543,6 +547,14 @@ func normalizeFlags(cmd *cobra.Command) error {
543547
err = invalidParameterError("-k", v, "1", "2")
544548
return pflag.NormalizedName("")
545549
}
550+
case printStatistics:
551+
switch v {
552+
case "0", "1":
553+
return pflag.NormalizedName(name)
554+
default:
555+
err = invalidParameterError("-p", v, "1")
556+
return pflag.NormalizedName("")
557+
}
546558
}
547559

548560
return pflag.NormalizedName(name)
@@ -812,6 +824,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
812824
s.SetupCloseHandler()
813825
defer s.StopCloseHandler()
814826
s.UnicodeOutputFile = args.UnicodeOutputFile
827+
s.PrintStatistics = args.PrintStatistics
815828

816829
if args.DisableCmd != nil {
817830
s.Cmd.DisableSysCommands(args.errorOnBlockedCmd())

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 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 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 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)