Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ 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.
- Sqlcmd defaults to a horizontal output format (space separated, no borders). To use the new ASCII table format, use the new `--ascii` command line option or set `SQLCMDFORMAT` to `ascii` (`-v SQLCMDFORMAT=ascii`).

```
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid
Expand Down
15 changes: 15 additions & 0 deletions build/buildfast.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@echo off

REM We get the value of the escape character by using PROMPT $E
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
set "DEL=%%a"
set "ESC=%%b"
)

REM Get Version Tag
for /f %%i in ('"git describe --tags --abbrev=0"') do set sqlcmdVersion=%%i

REM Generates sqlcmd.exe in the root dir of the repo
go build -o %~dp0..\sqlcmd.exe -ldflags="-X main.version=%sqlcmdVersion%" %~dp0..\cmd\modern

:end
15 changes: 11 additions & 4 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ type SQLCmdArguments struct {
ChangePasswordAndExit string
TraceFile string
// Keep Help at the end of the list
Help bool
Help bool
Ascii bool
}

func (args *SQLCmdArguments) useEnvVars() bool {
Expand Down Expand Up @@ -421,7 +422,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
rootCmd.Flags().BoolVarP(&args.UseAad, "use-aad", "G", false, localizer.Sprintf("Tells sqlcmd to use ActiveDirectory authentication. If no user name is provided, authentication method ActiveDirectoryDefault is used. If a password is provided, ActiveDirectoryPassword is used. Otherwise ActiveDirectoryInteractive is used"))
rootCmd.Flags().BoolVarP(&args.DisableVariableSubstitution, "disable-variable-substitution", "x", false, localizer.Sprintf("Causes sqlcmd to ignore scripting variables. This parameter is useful when a script contains many %s statements that may contain strings that have the same format as regular variables, such as $(variable_name)", localizer.InsertKeyword))
var variables map[string]string
rootCmd.Flags().StringToStringVarP(&args.Variables, "variables", "v", variables, localizer.Sprintf("Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits"))
rootCmd.Flags().StringToStringVarP(&args.Variables, "variables", "v", variables, localizer.Sprintf("Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits."))

rootCmd.Flags().IntVarP(&args.PacketSize, "packet-size", "a", 0, localizer.Sprintf("Requests a packet of a different size. This option sets the sqlcmd scripting variable %s. packet_size must be a value between 512 and 32767. The default = 4096. A larger packet size can enhance performance for execution of scripts that have lots of SQL statements between %s commands. You can request a larger packet size. However, if the request is denied, sqlcmd uses the server default for packet size", localizer.PacketSizeVar, localizer.BatchTerminatorGo))
rootCmd.Flags().IntVarP(&args.LoginTimeout, "login-timeOut", "l", -1, localizer.Sprintf("Specifies the number of seconds before a sqlcmd login to the go-mssqldb driver times out when you try to connect to a server. This option sets the sqlcmd scripting variable %s. The default value is 30. 0 means infinite", localizer.LoginTimeOutVar))
rootCmd.Flags().StringVarP(&args.WorkstationName, "workstation-name", "H", "", localizer.Sprintf("This option sets the sqlcmd scripting variable %s. The workstation name is listed in the hostname column of the sys.sysprocesses catalog view and can be returned using the stored procedure sp_who. If this option is not specified, the default is the current computer name. This name can be used to identify different sqlcmd sessions", localizer.WorkstationVar))
Expand All @@ -432,6 +434,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
// Can't use NoOptDefVal until this fix: https://github.com/spf13/cobra/issues/866
//rootCmd.Flags().Lookup(encryptConnection).NoOptDefVal = "true"
rootCmd.Flags().BoolVarP(&args.Vertical, "vertical", "", false, localizer.Sprintf("Prints the output in vertical format. This option sets the sqlcmd scripting variable %s to '%s'. The default is false", sqlcmd.SQLCMDFORMAT, "vert"))
rootCmd.Flags().BoolVarP(&args.Ascii, "ascii", "", false, localizer.Sprintf("Prints the output in ASCII table format. This option sets the sqlcmd scripting variable %s to '%s'. The default is false", sqlcmd.SQLCMDFORMAT, "ascii"))

_ = rootCmd.Flags().IntP(errorsToStderr, "r", -1, localizer.Sprintf("%s Redirects error messages with severity >= 11 output to stderr. Pass 1 to to redirect all errors including PRINT.", "-r[0 | 1]"))
rootCmd.Flags().IntVar(&args.DriverLoggingLevel, "driver-logging-level", 0, localizer.Sprintf("Level of mssql driver messages to print"))
rootCmd.Flags().BoolVarP(&args.ExitOnError, "exit-on-error", "b", false, localizer.Sprintf("Specifies that sqlcmd exits and returns a %s value when an error occurs", localizer.DosErrorLevel))
Expand Down Expand Up @@ -668,7 +672,10 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
if a.Vertical {
return "vert"
}
return "horizontal"
if a.Ascii {
return "ascii"
}
return ""
},
}
for varname, set := range varmap {
Expand Down Expand Up @@ -811,7 +818,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) {
}

s.Connect = &connectConfig
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces, args.getControlCharacterBehavior())
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(vars, args.TrimSpaces, args.getControlCharacterBehavior())
if args.OutputFile != "" {
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/sql/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (m *mssql) Connect(
m.console = nil
}
m.sqlcmd = sqlcmd.New(m.console, "", v)
m.sqlcmd.Format = sqlcmd.NewSQLCmdDefaultFormatter(false, sqlcmd.ControlIgnore)
m.sqlcmd.Format = sqlcmd.NewSQLCmdDefaultFormatter(v, false, sqlcmd.ControlIgnore)
connect := sqlcmd.ConnectSettings{
ServerName: fmt.Sprintf(
"%s,%#v",
Expand Down
2 changes: 1 addition & 1 deletion pkg/sqlcmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func TestListCommandUsesColorizer(t *testing.T) {
func TestListColorPrintsStyleSamples(t *testing.T) {
vars := InitializeVariables(false)
s := New(nil, "", vars)
s.Format = NewSQLCmdDefaultFormatter(false, ControlIgnore)
s.Format = NewSQLCmdDefaultFormatter(vars, false, ControlIgnore)
// force colorizer on
s.colorizer = color.New(true)
buf := &memoryBuffer{buf: new(bytes.Buffer)}
Expand Down
5 changes: 4 additions & 1 deletion pkg/sqlcmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ type sqlCmdFormatterType struct {
}

// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter
func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter {
func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter {
if vars.Format() == "ascii" {
return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb)
}
return &sqlCmdFormatterType{
removeTrailingSpaces: removeTrailingSpaces,
format: "horizontal",
Expand Down
174 changes: 174 additions & 0 deletions pkg/sqlcmd/format_ascii.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package sqlcmd

import (
"database/sql"
"os"
"strings"
"unicode/utf8"

"github.com/microsoft/go-sqlcmd/internal/color"
"golang.org/x/term"
)

type asciiFormatter struct {
*sqlCmdFormatterType
rows [][]string
}

func NewSQLCmdAsciiFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter {
return &asciiFormatter{
sqlCmdFormatterType: &sqlCmdFormatterType{
removeTrailingSpaces: removeTrailingSpaces,
format: "ascii",
colorizer: color.New(false),
ccb: ccb,
vars: vars,
},
}
}

func (f *asciiFormatter) BeginResultSet(cols []*sql.ColumnType) {
f.sqlCmdFormatterType.BeginResultSet(cols)
f.rows = make([][]string, 0)
}

func (f *asciiFormatter) AddRow(row *sql.Rows) string {
values, err := f.scanRow(row)
if err != nil {
f.mustWriteErr(err.Error())
return ""
}
f.rows = append(f.rows, values)
if len(values) > 0 {
return values[0]
}
return ""
}

func (f *asciiFormatter) EndResultSet() {
if len(f.rows) > 0 || len(f.columnDetails) > 0 {
f.printAsciiTable()
}
f.rows = nil
}

func (f *asciiFormatter) printAsciiTable() {
colWidths := make([]int, len(f.columnDetails))

for i, c := range f.columnDetails {
colWidths[i] = utf8.RuneCountInString(c.col.Name())
}

for _, row := range f.rows {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you build up the width metrics within AddRow instead of waiting for the end of the resultset? There could be thousands of rows and this extra iteration over them can take a long time

Copy link
Author

Choose a reason for hiding this comment

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

Done.

for i, val := range row {
if i < len(colWidths) {
l := utf8.RuneCountInString(val)
if l > colWidths[i] {
colWidths[i] = l
}
}
}
}

maxWidth := int(f.vars.ScreenWidth())
if maxWidth <= 0 {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {
maxWidth = w - 1
} else {
maxWidth = 1000000
}
}

totalWidth := 1
for _, w := range colWidths {
totalWidth += w + 3
}

if totalWidth <= maxWidth {
f.printTableSegment(colWidths, 0, len(colWidths)-1)
} else {
startCol := 0
for startCol < len(colWidths) {
currentWidth := 1
endCol := startCol
for endCol < len(colWidths) {
w := colWidths[endCol] + 3
if currentWidth+w > maxWidth {
break
}
currentWidth += w
endCol++
}

if endCol == startCol {
endCol++
}

f.printTableSegment(colWidths, startCol, endCol-1)
startCol = endCol
}
}
}

func (f *asciiFormatter) printTableSegment(colWidths []int, startCol, endCol int) {
if startCol > endCol {
return
}

divider := "+"
for i := startCol; i <= endCol; i++ {
divider += strings.Repeat("-", colWidths[i]+2) + "+"
}
f.writeOut(divider+SqlcmdEol, color.TextTypeNormal)

header := "|"
for i := startCol; i <= endCol; i++ {
name := f.columnDetails[i].col.Name()
header += " " + padRightString(name, colWidths[i]) + " |"
}
f.writeOut(header+SqlcmdEol, color.TextTypeNormal)
f.writeOut(divider+SqlcmdEol, color.TextTypeNormal)

for _, row := range f.rows {
line := "|"
for i := startCol; i <= endCol; i++ {
val := ""
if i < len(row) {
val = row[i]
}
isNumeric := isNumericType(f.columnDetails[i].col.DatabaseTypeName())

if isNumeric {
line += " " + padLeftString(val, colWidths[i]) + " |"
} else {
line += " " + padRightString(val, colWidths[i]) + " |"
}
}
f.writeOut(line+SqlcmdEol, color.TextTypeNormal)
}
f.writeOut(divider+SqlcmdEol, color.TextTypeNormal)
}

func padRightString(s string, width int) string {
l := utf8.RuneCountInString(s)
if l >= width {
return s
}
return s + strings.Repeat(" ", width-l)
}

func padLeftString(s string, width int) string {
l := utf8.RuneCountInString(s)
if l >= width {
return s
}
return strings.Repeat(" ", width-l) + s
}

func isNumericType(typeName string) bool {
switch typeName {
case "TINYINT", "SMALLINT", "INT", "BIGINT", "REAL", "FLOAT", "DECIMAL", "NUMERIC", "MONEY", "SMALLMONEY":
return true
}
return false
}
63 changes: 63 additions & 0 deletions pkg/sqlcmd/format_ascii_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package sqlcmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAsciiFormatter(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
if s.db == nil {
t.Skip("No database connection available")
}
defer buf.Close()

// Set format to ascii
s.vars.Set(SQLCMDFORMAT, "ascii")
s.Format = NewSQLCmdDefaultFormatter(s.vars, false, ControlIgnore)

err := runSqlCmd(t, s, []string{"select 1 as id, 'test' as name", "GO"})
assert.NoError(t, err, "runSqlCmd returned error")

expected := `+----+------+` + SqlcmdEol +
`| id | name |` + SqlcmdEol +
`+----+------+` + SqlcmdEol +
`| 1 | test |` + SqlcmdEol +
`+----+------+` + SqlcmdEol +
`(1 row affected)` + SqlcmdEol

assert.Equal(t, expected, buf.buf.String())
}

func TestAsciiFormatterWrapping(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
if s.db == nil {
t.Skip("No database connection available")
}
defer buf.Close()

s.vars.Set(SQLCMDFORMAT, "ascii")
s.vars.Set(SQLCMDCOLWIDTH, "20") // Small width to force wrapping
s.Format = NewSQLCmdDefaultFormatter(s.vars, false, ControlIgnore)

// Select 3 columns that won't fit in 20 chars
err := runSqlCmd(t, s, []string{"select 1 as id, 'test' as name, '0123456789' as descr", "GO"})
assert.NoError(t, err, "runSqlCmd returned error")

expectedPart1 := `+----+------+` + SqlcmdEol +
`| id | name |` + SqlcmdEol +
`+----+------+` + SqlcmdEol +
`| 1 | test |` + SqlcmdEol +
`+----+------+` + SqlcmdEol

expectedPart2 := `+------------+` + SqlcmdEol +
`| descr |` + SqlcmdEol +
`+------------+` + SqlcmdEol +
`| 0123456789 |` + SqlcmdEol +
`+------------+` + SqlcmdEol +
`(1 row affected)` + SqlcmdEol

assert.Contains(t, buf.buf.String(), expectedPart1)
assert.Contains(t, buf.buf.String(), expectedPart2)
}
10 changes: 6 additions & 4 deletions pkg/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,13 @@ func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) {
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
s := New(nil, "", v)
s.Connect = newConnect(t)
s.Format = NewSQLCmdDefaultFormatter(true, ControlIgnore)
s.Format = NewSQLCmdDefaultFormatter(v, true, ControlIgnore)
buf := &memoryBuffer{buf: new(bytes.Buffer)}
s.SetOutput(buf)
err := s.ConnectDb(nil, true)
assert.NoError(t, err, "s.ConnectDB")
if err != nil {
t.Logf("ConnectDb failed: %v", err)
}
return s, buf
}

Expand All @@ -633,7 +635,7 @@ func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) {
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
s := New(nil, "", v)
s.Connect = newConnect(t)
s.Format = NewSQLCmdDefaultFormatter(true, ControlIgnore)
s.Format = NewSQLCmdDefaultFormatter(v, true, ControlIgnore)
file, err := os.CreateTemp("", "sqlcmdout")
assert.NoError(t, err, "os.CreateTemp")
s.SetOutput(file)
Expand All @@ -651,7 +653,7 @@ func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File)
v.Set(SQLCMDMAXVARTYPEWIDTH, "0")
s := New(nil, "", v)
s.Connect = newConnect(t)
s.Format = NewSQLCmdDefaultFormatter(true, ControlIgnore)
s.Format = NewSQLCmdDefaultFormatter(v, true, ControlIgnore)
outfile, err := os.CreateTemp("", "sqlcmdout")
assert.NoError(t, err, "os.CreateTemp")
errfile, err := os.CreateTemp("", "sqlcmderr")
Expand Down
5 changes: 5 additions & 0 deletions pkg/sqlcmd/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ func (v Variables) Format() string {
switch v[SQLCMDFORMAT] {
case "vert", "vertical":
return "vertical"
case "ascii":
return "ascii"
case "horiz", "horizontal":
return "horizontal"
}
return "horizontal"
}
Expand Down Expand Up @@ -246,6 +250,7 @@ func InitializeVariables(fromEnvironment bool) *Variables {
SQLCMDUSER: "",
SQLCMDUSEAAD: "",
SQLCMDCOLORSCHEME: "",
SQLCMDFORMAT: "",
}
hostname, _ := os.Hostname()
variables.Set(SQLCMDWORKSTATION, hostname)
Expand Down
Loading