Skip to content
Closed
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ client_interface_name go-mssqldb
program_name sqlcmd
```

- The `-p[1]` flag prints performance statistics after each result set. Use `-p` for human-readable format or `-p1` for colon-separated format suitable for scripts.
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states the flag "prints performance statistics after each result set" but the implementation prints statistics after each batch (GO command), not after each individual result set within a batch. Consider updating the documentation to say "after each batch" to accurately reflect the implementation behavior.

Copilot uses AI. Check for mistakes.

```
1> select 1
2> go

-----------
1

(1 row affected)
Network packet size (bytes): 4096
1 xact(s):
Clock Time (ms.): total 15 avg 15 (66.67 xacts per sec.)
```

- `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
16 changes: 16 additions & 0 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ type SQLCmdArguments struct {
ChangePassword string
ChangePasswordAndExit string
TraceFile string
// PrintStatistics prints performance statistics after each batch
// nil = disabled, 0 = human-readable, 1 = colon-separated
PrintStatistics *int
// Keep Help at the end of the list
Help bool
}
Expand Down Expand Up @@ -126,6 +129,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 +334,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 +398,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 @@ -474,6 +480,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
_ = rootCmd.Flags().BoolP("enable-quoted-identifiers", "I", true, localizer.Sprintf("Provided for backward compatibility. Quoted identifiers are always enabled"))
_ = 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().IntP(printStatistics, "p", -1, localizer.Sprintf("%s Print performance statistics for every result set. Pass 1 to output in colon-separated format", "-p[1]"))
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text says "Print performance statistics for every result set" but the implementation actually prints statistics after each batch (GO command), not after each result set within a batch. Consider updating the help text to say "Print performance statistics after each batch" to accurately reflect the implementation.

Copilot uses AI. Check for mistakes.
rootCmd.Flags().BoolVarP(&args.EchoInput, "echo-input", "e", false, localizer.Sprintf("Echo input"))
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"))
Expand Down Expand Up @@ -543,6 +550,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 @@ -829,6 +844,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {

s.Connect = &connectConfig
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
s.PrintStatistics = args.PrintStatistics
if args.OutputFile != "" {
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
}},
// Test -p flag for performance statistics
{[]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{"--print-statistics", "0"}, func(args SQLCmdArguments) bool {
return args.PrintStatistics != nil && *args.PrintStatistics == 0
}},
}

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

for _, test := range commands {
Expand Down
63 changes: 63 additions & 0 deletions pkg/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ type Sqlcmd struct {
EchoInput bool
colorizer color.Colorizer
termchan chan os.Signal
// PrintStatistics controls whether performance statistics are printed after each batch
// nil = disabled, 0 = human-readable format, 1 = colon-separated format
PrintStatistics *int
// stats tracks cumulative statistics for the session
stats *SessionStats
}

// SessionStats tracks cumulative performance statistics for a sqlcmd session
type SessionStats struct {
TotalTransactions int
TotalTimeMs float64
}

// New creates a new Sqlcmd instance.
Expand Down Expand Up @@ -420,6 +431,9 @@ func (s *Sqlcmd) getRunnableQuery(q string) string {
// -101: No rows found
// -102: Conversion error occurred when selecting return value
func (s *Sqlcmd) runQuery(query string) (int, error) {
// Start timing for statistics
startTime := time.Now()

retcode := -101
s.Format.BeginBatch(query, s.vars, s.GetOutput(), s.GetError())
ctx := context.Background()
Expand Down Expand Up @@ -508,6 +522,13 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
}
}
s.Format.EndBatch()

// Print statistics if enabled
if s.PrintStatistics != nil {
elapsed := time.Since(startTime)
s.printStatistics(elapsed)
}

return retcode, qe
}

Expand Down Expand Up @@ -552,6 +573,48 @@ func (s *Sqlcmd) handleError(retcode *int, err error) error {
return nil
}

// printStatistics prints performance statistics for the query
func (s *Sqlcmd) printStatistics(elapsed time.Duration) {
if s.stats == nil {
s.stats = &SessionStats{}
}

// Update cumulative statistics
s.stats.TotalTransactions++
elapsedMs := float64(elapsed.Milliseconds())
s.stats.TotalTimeMs += elapsedMs

// Calculate statistics
avgMs := s.stats.TotalTimeMs / float64(s.stats.TotalTransactions)
var xactsPerSec float64
if s.stats.TotalTimeMs > 0 {
xactsPerSec = float64(s.stats.TotalTransactions) / (s.stats.TotalTimeMs / 1000.0)
}

// Get packet size from connection settings
packetSize := s.Connect.PacketSize
if packetSize == 0 {
packetSize = 4096 // default
}

out := s.GetOutput()

if *s.PrintStatistics == 1 {
// Colon-separated format for spreadsheet/script processing
_, _ = out.Write([]byte(localizer.Sprintf("Network packet size (bytes):%d%s", packetSize, SqlcmdEol)))
_, _ = out.Write([]byte(localizer.Sprintf("%d xact(s):%s", s.stats.TotalTransactions, SqlcmdEol)))
_, _ = out.Write([]byte(localizer.Sprintf("Clock Time (ms.): total:%d:avg:%.2f:(%.4f xacts per sec.)%s",
int(s.stats.TotalTimeMs), avgMs, xactsPerSec, SqlcmdEol)))
} else {
// Human-readable format
_, _ = out.Write([]byte(SqlcmdEol))
_, _ = out.Write([]byte(localizer.Sprintf("Network packet size (bytes): %d%s", packetSize, SqlcmdEol)))
_, _ = out.Write([]byte(localizer.Sprintf("%d xact(s):%s", s.stats.TotalTransactions, SqlcmdEol)))
_, _ = out.Write([]byte(localizer.Sprintf("Clock Time (ms.): total %d avg %.2f (%.4f xacts per sec.)%s",
int(s.stats.TotalTimeMs), avgMs, xactsPerSec, SqlcmdEol)))
Comment on lines +606 to +614
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README example shows xacts per sec with 2 decimal places (66.67) but the code formats it with 4 decimal places using %.4f. For consistency with the documentation example, consider using %.2f instead of %.4f for the xactsPerSec value in both format modes.

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +614
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description and README examples show the average time as an integer (e.g., "avg:15" or "avg 15"), but the code formats it with %.2f which will display decimal places (e.g., "15.00"). If matching ODBC sqlcmd output exactly is important, consider using %d formatting when the average is a whole number, or using %.0f to show no decimal places when they're not significant. Otherwise, %.2f is acceptable for consistency.

Copilot uses AI. Check for mistakes.
}
}

// Log attempts to write driver traces to the current output. It ignores errors
func (s Sqlcmd) Log(_ context.Context, _ msdsn.Log, msg string) {
_, _ = s.GetOutput().Write([]byte("DRIVER:" + msg))
Expand Down
Loading