Skip to content

Commit e4621c7

Browse files
Add -p[1] performance statistics flag
Implements the -p[1] flag for printing performance statistics after each result set: - -p or -p0: Human-readable format with network packet size, transaction count, and timing - -p1: Colon-separated format for spreadsheet/script processing Statistics include: - Network packet size (bytes) - Transaction count - Clock time: total, average, and transactions per second Fixes compatibility with ODBC sqlcmd -p flag.
1 parent 758fca9 commit e4621c7

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

cmd/sqlcmd/sqlcmd.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ type SQLCmdArguments struct {
8282
ChangePassword string
8383
ChangePasswordAndExit string
8484
TraceFile string
85+
// PrintStatistics prints performance statistics after each batch
86+
// nil = disabled, 0 = human-readable, 1 = colon-separated
87+
PrintStatistics *int
8588
// Keep Help at the end of the list
8689
Help bool
8790
}
@@ -126,6 +129,7 @@ const (
126129
disableCmdAndWarn = "disable-cmd-and-warn"
127130
listServers = "list-servers"
128131
removeControlCharacters = "remove-control-characters"
132+
printStatistics = "print-statistics"
129133
)
130134

131135
func encryptConnectionAllowsTLS(value string) bool {
@@ -330,6 +334,7 @@ func checkDefaultValue(args []string, i int) (val string) {
330334
'k': "0",
331335
'L': "|", // | is the sentinel for no value since users are unlikely to use it. It's "reserved" in most shells
332336
'X': "0",
337+
'p': "0",
333338
}
334339
if isFlag(args[i]) && len(args[i]) == 2 && (len(args) == i+1 || args[i+1][0] == '-') {
335340
if v, ok := flags[rune(args[i][1])]; ok {
@@ -393,6 +398,7 @@ func SetScreenWidthFlags(args *SQLCmdArguments, rootCmd *cobra.Command) {
393398
args.DisableCmd = getOptionalIntArgument(rootCmd, disableCmdAndWarn)
394399
args.ErrorsToStderr = getOptionalIntArgument(rootCmd, errorsToStderr)
395400
args.RemoveControlCharacters = getOptionalIntArgument(rootCmd, removeControlCharacters)
401+
args.PrintStatistics = getOptionalIntArgument(rootCmd, printStatistics)
396402
}
397403

398404
func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
@@ -474,6 +480,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
474480
_ = rootCmd.Flags().BoolP("enable-quoted-identifiers", "I", true, localizer.Sprintf("Provided for backward compatibility. Quoted identifiers are always enabled"))
475481
_ = rootCmd.Flags().BoolP("client-regional-setting", "R", false, localizer.Sprintf("Provided for backward compatibility. Client regional settings are not used"))
476482
_ = 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]"))
483+
_ = rootCmd.Flags().IntP(printStatistics, "p", -1, localizer.Sprintf("%s Print performance statistics for every result set. Pass 1 to output in colon-separated format", "-p[1]"))
477484
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
478485
rootCmd.Flags().IntVarP(&args.QueryTimeout, "query-timeout", "t", 0, "Query timeout")
479486
rootCmd.Flags().BoolVarP(&args.EnableColumnEncryption, "enable-column-encryption", "g", false, localizer.Sprintf("Enable column encryption"))
@@ -543,6 +550,14 @@ func normalizeFlags(cmd *cobra.Command) error {
543550
err = invalidParameterError("-k", v, "1", "2")
544551
return pflag.NormalizedName("")
545552
}
553+
case printStatistics:
554+
switch v {
555+
case "0", "1":
556+
return pflag.NormalizedName(name)
557+
default:
558+
err = invalidParameterError("-p", v, "0", "1")
559+
return pflag.NormalizedName("")
560+
}
546561
}
547562

548563
return pflag.NormalizedName(name)
@@ -829,6 +844,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
829844

830845
s.Connect = &connectConfig
831846
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
847+
s.PrintStatistics = args.PrintStatistics
832848
if args.OutputFile != "" {
833849
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
834850
if err != nil {

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
123123
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
124124
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
125125
}},
126+
// Test -p flag for performance statistics
127+
{[]string{"-p"}, func(args SQLCmdArguments) bool {
128+
return args.PrintStatistics != nil && *args.PrintStatistics == 0
129+
}},
130+
{[]string{"-p", "1"}, func(args SQLCmdArguments) bool {
131+
return args.PrintStatistics != nil && *args.PrintStatistics == 1
132+
}},
133+
{[]string{"--print-statistics", "0"}, func(args SQLCmdArguments) bool {
134+
return args.PrintStatistics != nil && *args.PrintStatistics == 0
135+
}},
126136
}
127137

128138
for _, test := range commands {
@@ -178,6 +188,7 @@ func TestInvalidCommandLine(t *testing.T) {
178188
{[]string{"-N", "optional", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
179189
{[]string{"-N", "disable", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
180190
{[]string{"-N", "strict", "-F", "myserver.domain.com", "-J", "/path/to/cert.pem"}, "The -F and the -J options are mutually exclusive."},
191+
{[]string{"-p", "2"}, "'-p 2': Unexpected argument. Argument value has to be one of [0 1]."},
181192
}
182193

183194
for _, test := range commands {

pkg/sqlcmd/sqlcmd.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ type Sqlcmd struct {
8888
EchoInput bool
8989
colorizer color.Colorizer
9090
termchan chan os.Signal
91+
// PrintStatistics controls whether performance statistics are printed after each batch
92+
// nil = disabled, 0 = human-readable format, 1 = colon-separated format
93+
PrintStatistics *int
94+
// stats tracks cumulative statistics for the session
95+
stats *SessionStats
96+
}
97+
98+
// SessionStats tracks cumulative performance statistics for a sqlcmd session
99+
type SessionStats struct {
100+
TotalTransactions int
101+
TotalTimeMs float64
91102
}
92103

93104
// New creates a new Sqlcmd instance.
@@ -420,6 +431,9 @@ func (s *Sqlcmd) getRunnableQuery(q string) string {
420431
// -101: No rows found
421432
// -102: Conversion error occurred when selecting return value
422433
func (s *Sqlcmd) runQuery(query string) (int, error) {
434+
// Start timing for statistics
435+
startTime := time.Now()
436+
423437
retcode := -101
424438
s.Format.BeginBatch(query, s.vars, s.GetOutput(), s.GetError())
425439
ctx := context.Background()
@@ -508,6 +522,13 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
508522
}
509523
}
510524
s.Format.EndBatch()
525+
526+
// Print statistics if enabled
527+
if s.PrintStatistics != nil {
528+
elapsed := time.Since(startTime)
529+
s.printStatistics(elapsed)
530+
}
531+
511532
return retcode, qe
512533
}
513534

@@ -552,6 +573,48 @@ func (s *Sqlcmd) handleError(retcode *int, err error) error {
552573
return nil
553574
}
554575

576+
// printStatistics prints performance statistics for the query
577+
func (s *Sqlcmd) printStatistics(elapsed time.Duration) {
578+
if s.stats == nil {
579+
s.stats = &SessionStats{}
580+
}
581+
582+
// Update cumulative statistics
583+
s.stats.TotalTransactions++
584+
elapsedMs := float64(elapsed.Milliseconds())
585+
s.stats.TotalTimeMs += elapsedMs
586+
587+
// Calculate statistics
588+
avgMs := s.stats.TotalTimeMs / float64(s.stats.TotalTransactions)
589+
var xactsPerSec float64
590+
if s.stats.TotalTimeMs > 0 {
591+
xactsPerSec = float64(s.stats.TotalTransactions) / (s.stats.TotalTimeMs / 1000.0)
592+
}
593+
594+
// Get packet size from connection settings
595+
packetSize := s.Connect.PacketSize
596+
if packetSize == 0 {
597+
packetSize = 4096 // default
598+
}
599+
600+
out := s.GetOutput()
601+
602+
if *s.PrintStatistics == 1 {
603+
// Colon-separated format for spreadsheet/script processing
604+
_, _ = out.Write([]byte(localizer.Sprintf("Network packet size (bytes):%d%s", packetSize, SqlcmdEol)))
605+
_, _ = out.Write([]byte(localizer.Sprintf("%d xact(s):%s", s.stats.TotalTransactions, SqlcmdEol)))
606+
_, _ = out.Write([]byte(localizer.Sprintf("Clock Time (ms.): total:%d:avg:%.2f:(%.4f xacts per sec.)%s",
607+
int(s.stats.TotalTimeMs), avgMs, xactsPerSec, SqlcmdEol)))
608+
} else {
609+
// Human-readable format
610+
_, _ = out.Write([]byte(SqlcmdEol))
611+
_, _ = out.Write([]byte(localizer.Sprintf("Network packet size (bytes): %d%s", packetSize, SqlcmdEol)))
612+
_, _ = out.Write([]byte(localizer.Sprintf("%d xact(s):%s", s.stats.TotalTransactions, SqlcmdEol)))
613+
_, _ = out.Write([]byte(localizer.Sprintf("Clock Time (ms.): total %d avg %.2f (%.4f xacts per sec.)%s",
614+
int(s.stats.TotalTimeMs), avgMs, xactsPerSec, SqlcmdEol)))
615+
}
616+
}
617+
555618
// Log attempts to write driver traces to the current output. It ignores errors
556619
func (s Sqlcmd) Log(_ context.Context, _ msdsn.Log, msg string) {
557620
_, _ = s.GetOutput().Write([]byte("DRIVER:" + msg))

0 commit comments

Comments
 (0)