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.)
```
Comment thread
dlevy-msft-sql marked this conversation as resolved.

```
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]"))
Comment thread
dlevy-msft-sql marked this conversation as resolved.
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
10 changes: 8 additions & 2 deletions pkg/sqlcmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ func exitCommand(s *Sqlcmd, args []string, line uint) error {

if len(query1) > 0 || len(query2) > 0 {
query := query1 + SqlcmdEol + query2
s.Exitcode, _ = s.runQuery(query)
var elapsedMs int64
s.Exitcode, elapsedMs, _ = s.runQuery(query)
s.printStatistics(elapsedMs, 1, s.GetOutput())
}
return ErrExitRequested
}
Expand Down Expand Up @@ -290,12 +292,16 @@ func goCommand(s *Sqlcmd, args []string, line uint) error {
return nil
}
query = s.getRunnableQuery(query)
var totalElapsedMs int64
for i := 0; i < n; i++ {
if retcode, err := s.runQuery(query); err != nil {
retcode, elapsedMs, err := s.runQuery(query)
totalElapsedMs += elapsedMs
if err != nil {
s.Exitcode = retcode
return err
}
}
s.printStatistics(totalElapsedMs, n, s.GetOutput())
s.batch.Reset(nil)
return nil
}
Expand Down
59 changes: 53 additions & 6 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 @@ -413,13 +416,14 @@ func (s *Sqlcmd) getRunnableQuery(q string) string {
return b.String()
}

// runQuery runs the query and prints the results
// The return value is based on the first cell of the last column of the last result set.
// runQuery runs the query and prints the results.
// Returns (exitcode, elapsedMs, error).
// The exitcode is based on the first cell of the last column of the last result set.
// If it's numeric, it will be converted to int
// -100 : Error encountered prior to selecting return value
// -101: No rows found
// -102: Conversion error occurred when selecting return value
func (s *Sqlcmd) runQuery(query string) (int, error) {
func (s *Sqlcmd) runQuery(query string) (int, int64, error) {
retcode := -101
s.Format.BeginBatch(query, s.vars, s.GetOutput(), s.GetError())
ctx := context.Background()
Expand All @@ -430,6 +434,7 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
ctx = ct
}
retmsg := &sqlexp.ReturnMessage{}
startTime := time.Now()
rows, qe := s.db.QueryContext(ctx, query, retmsg)
if qe != nil {
s.Format.AddError(qe)
Expand Down Expand Up @@ -507,8 +512,9 @@ func (s *Sqlcmd) runQuery(query string) (int, error) {
s.Format.EndResultSet()
}
}
elapsedMs := time.Since(startTime).Milliseconds()
s.Format.EndBatch()
return retcode, qe
return retcode, elapsedMs, qe
}

// returns ErrExitRequested if the error is a SQL error and satisfies the connection's error handling configuration
Expand Down Expand Up @@ -580,3 +586,44 @@ func (s *Sqlcmd) SetupCloseHandler() {
func (s *Sqlcmd) StopCloseHandler() {
signal.Stop(s.termchan)
}

// printStatistics prints performance statistics after a batch execution
// if PrintStatistics is enabled. The out parameter controls where the
// statistics are written (typically s.GetOutput(), or s.GetStat() when
// :perftrace redirection is active).
func (s *Sqlcmd) printStatistics(elapsedMs int64, numBatches int, out io.Writer) {
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 rate calculations, but display actual 0 as "< 1"
displayElapsedMs := elapsedMs
calcElapsedMs := elapsedMs
if calcElapsedMs < 1 {
calcElapsedMs = 1
}

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

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, displayElapsedMs, avgTime, batchesPerSec)
} else {
// Standard format
_, _ = fmt.Fprintf(out, "\nNetwork packet size (bytes): %d\n", packetSize)
_, _ = fmt.Fprintf(out, "%d xact[s]:\n", numBatches)
if displayElapsedMs < 1 {
_, _ = fmt.Fprintf(out, "Clock Time (ms.): total < 1 avg %.2f (%.2f xacts per sec.)\n", avgTime, batchesPerSec)
} else {
_, _ = fmt.Fprintf(out, "Clock Time (ms.): total %7d avg %.2f (%.2f xacts per sec.)\n", displayElapsedMs, avgTime, batchesPerSec)
}
}
}
Loading