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`).
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated

```
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
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
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."))
Comment thread
shueybubbles marked this conversation as resolved.
Outdated

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
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
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)
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
}

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 {
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
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)
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated

header := "|"
for i := startCol; i <= endCol; i++ {
name := f.columnDetails[i].col.Name()
header += " " + padRightString(name, colWidths[i]) + " |"
}
f.writeOut(header+SqlcmdEol, color.TextTypeNormal)
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
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]) + " |"
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
}
}
f.writeOut(line+SqlcmdEol, color.TextTypeNormal)
Comment thread
0x7FFFFFFFFFFFFFFF marked this conversation as resolved.
Outdated
}
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