Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -30,6 +30,7 @@ We will be implementing command line switches and behaviors over time. Several s

- `: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` user name 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 `-F vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable.

### Azure Active Directory Authentication

Expand Down
2 changes: 2 additions & 0 deletions cmd/sqlcmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type SQLCmdArguments struct {
ExitOnError bool `short:"b" help:"Specifies that sqlcmd exits and returns a DOS ERRORLEVEL value when an error occurs."`
ErrorSeverityLevel uint8 `short:"V" help:"Controls the severity level that is used to set the ERRORLEVEL variable on exit."`
ErrorLevel int `short:"m" help:"Controls which error messages are sent to stdout. Messages that have severity level greater than or equal to this level are sent."`
Format string `short:"F" help:"Specifies the formatting for results." default:"horiz" enum:"horiz,horizontal,vert,vertical"`
}

// Validate accounts for settings not described by Kong attributes
Expand Down Expand Up @@ -141,6 +142,7 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) {
sqlcmd.SQLCMDCOLWIDTH: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDMAXVARTYPEWIDTH: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDMAXFIXEDTYPEWIDTH: func(a *SQLCmdArguments) string { return "" },
sqlcmd.SQLCMDFORMAT: func(a *SQLCmdArguments) string { return a.Format },
}
for varname, set := range varmap {
val := set(args)
Expand Down
4 changes: 4 additions & 0 deletions cmd/sqlcmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-b", "-m", "15", "-V", "20"}, func(args SQLCmdArguments) bool {
return args.ExitOnError && args.ErrorLevel == 15 && args.ErrorSeverityLevel == 20
}},
{[]string{"-F", "vert"}, func(args SQLCmdArguments) bool {
return args.Format == "vert"
}},
}

for _, test := range commands {
Expand All @@ -96,6 +99,7 @@ func TestInvalidCommandLine(t *testing.T) {
{[]string{"-E", "-U", "someuser"}, "--use-trusted-connection and --user-name can't be used together"},
// the test prefix is a kong artifact https://github.com/alecthomas/kong/issues/221
{[]string{"-a", "100"}, "test: '-a 100': Packet size has to be a number between 512 and 32767."},
{[]string{"-F", "what"}, "--format must be one of \"horiz\",\"horizontal\",\"vert\",\"vertical\" but got \"what\""},
}

for _, test := range commands {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/denisenkom/go-mssqldb v0.12.0
github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188
github.com/google/uuid v1.2.0
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.7.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZ
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4=
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
Expand Down
100 changes: 71 additions & 29 deletions pkg/sqlcmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

mssql "github.com/denisenkom/go-mssqldb"
"github.com/google/uuid"
)

const (
Expand Down Expand Up @@ -58,6 +59,8 @@ type columnDetail struct {
}

// The default formatter based on the native sqlcmd style
// It supports both horizontal (default) and vertical layout for results.
// Both vertical and horizontal layouts respect column widths set by SQLCMD variables.
type sqlCmdFormatterType struct {
out io.Writer
err io.Writer
Expand All @@ -68,12 +71,15 @@ type sqlCmdFormatterType struct {
columnDetails []columnDetail
rowcount int
writepos int64
format string
maxColNameLen int
}

// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter
func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool) Formatter {
return &sqlCmdFormatterType{
removeTrailingSpaces: removeTrailingSpaces,
format: "horizontal",
}
}

Expand Down Expand Up @@ -115,6 +121,7 @@ func (f *sqlCmdFormatterType) BeginBatch(_ string, vars *Variables, out io.Write
f.err = err
f.vars = vars
f.colsep = vars.ColumnSeparator()
f.format = vars.Format()
}

func (f *sqlCmdFormatterType) EndBatch() {
Expand All @@ -125,8 +132,8 @@ func (f *sqlCmdFormatterType) EndBatch() {
// base our numbers for most types on https://docs.microsoft.com/sql/odbc/reference/appendixes/column-size
func (f *sqlCmdFormatterType) BeginResultSet(cols []*sql.ColumnType) {
f.rowcount = 0
f.columnDetails = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth())
if f.vars.RowsBetweenHeaders() > -1 {
f.columnDetails, f.maxColNameLen = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth())
if f.vars.RowsBetweenHeaders() > -1 && f.format == "horizontal" {
f.printColumnHeadings()
}
}
Expand All @@ -144,25 +151,41 @@ func (f *sqlCmdFormatterType) AddRow(row *sql.Rows) string {
f.mustWriteErr(err.Error())
return retval
}
retval = values[0]
if f.format == "horizontal" {
// values are the full values, look at the displaywidth of each column and truncate accordingly
for i, v := range values {
if i > 0 {
f.writeOut(f.vars.ColumnSeparator())
}
f.printColumnValue(v, i)
}
f.rowcount++
gap := f.vars.RowsBetweenHeaders()
if gap > 0 && (int64(f.rowcount)%gap == 0) {
f.writeOut(SqlcmdEol)
f.printColumnHeadings()
}
} else {
f.addVerticalRow(values)
}
f.writeOut(SqlcmdEol)
return retval

// values are the full values, look at the displaywidth of each column and truncate accordingly
}

func (f *sqlCmdFormatterType) addVerticalRow(values []string) {
for i, v := range values {
if i > 0 {
f.writeOut(f.vars.ColumnSeparator())
} else {
retval = v
if f.vars.RowsBetweenHeaders() > -1 {
builder := new(strings.Builder)
name := f.columnDetails[i].col.Name()
builder.WriteString(name)
builder = padRight(builder, int64(f.maxColNameLen-len(name)+1), " ")
f.writeOut(builder.String())
}
f.printColumnValue(v, i)
}
f.rowcount++
gap := f.vars.RowsBetweenHeaders()
if gap > 0 && (int64(f.rowcount)%gap == 0) {
f.writeOut(SqlcmdEol)
f.printColumnHeadings()
}
f.writeOut(SqlcmdEol)
return retval

}

// Writes a non-error message to the designated message writer
Expand Down Expand Up @@ -266,12 +289,17 @@ func fitToScreen(s *strings.Builder, width int64) *strings.Builder {
}

// Given the array of driver-provided columnType values and the sqlcmd size limits,
// return an array of columnDetail objects describing the output format for each column
func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) (columnDetails []columnDetail) {
columnDetails = make([]columnDetail, len(cols))
// Return an array of columnDetail objects describing the output format for each column.
// Return the length of the longest column name.
func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) ([]columnDetail, int) {
columnDetails := make([]columnDetail, len(cols))
maxNameLen := 0
for i, c := range cols {
length, _ := c.Length()
nameLen := int64(len([]rune(c.Name())))
if nameLen > int64(maxNameLen) {
maxNameLen = int(nameLen)
}
columnDetails[i].col = *c
columnDetails[i].leftJustify = true
columnDetails[i].zeroesAfterDecimal = false
Expand Down Expand Up @@ -381,7 +409,7 @@ func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) (col
columnDetails[i].displayWidth = 0
}
}
return columnDetails
return columnDetails, maxNameLen
}

// scanRow fetches the next row and converts each value to the appropriate string representation
Expand All @@ -403,6 +431,18 @@ func (f *sqlCmdFormatterType) scanRow(rows *sql.Rows) ([]string, error) {
case []byte:
if isBinaryDataType(&f.columnDetails[n].col) {
row[n] = decodeBinary(x)
} else if f.columnDetails[n].col.DatabaseTypeName() == "UNIQUEIDENTIFIER" {
// Unscramble the guid
// see https://github.com/denisenkom/go-mssqldb/issues/56
x[0], x[1], x[2], x[3] = x[3], x[2], x[1], x[0]
x[4], x[5] = x[5], x[4]
x[6], x[7] = x[7], x[6]
if guid, err := uuid.FromBytes(x); err == nil {
row[n] = guid.String()
} else {
// this should never happen
row[n] = uuid.New().String()
}
} else {
row[n] = string(x)
}
Expand Down Expand Up @@ -445,20 +485,22 @@ func (f *sqlCmdFormatterType) printColumnValue(val string, col int) {

s.WriteString(val)
r := []rune(val)
if !f.removeTrailingSpaces {
if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) {
padding := c.displayWidth - min64(c.displayWidth, int64(len(r)))
if padding > 0 {
if c.leftJustify {
s = padRight(s, padding, " ")
} else {
s = padLeft(s, padding, " ")
if f.format == "horizontal" {
if !f.removeTrailingSpaces {
if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) {
padding := c.displayWidth - min64(c.displayWidth, int64(len(r)))
if padding > 0 {
if c.leftJustify {
s = padRight(s, padding, " ")
} else {
s = padLeft(s, padding, " ")
}
}
}
}
}

r = []rune(s.String())
r = []rune(s.String())
}
if c.displayWidth > 0 && int64(len(r)) > c.displayWidth {
s.Reset()
s.WriteString(string(r[:c.displayWidth]))
Expand Down
9 changes: 6 additions & 3 deletions pkg/sqlcmd/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestCalcColumnDetails(t *testing.T) {
variable int64
query string
details []columnDetail
max int
}

tests := []colTest{
Expand All @@ -53,25 +54,27 @@ func TestCalcColumnDetails(t *testing.T) {
{leftJustify: false, displayWidth: 23},
{leftJustify: true, displayWidth: 6},
},
12,
},
}

db, err := ConnectDb(t)
if assert.NoError(t, err, "ConnectDB failed") {
defer db.Close()
for _, test := range tests {
for x, test := range tests {
rows, err := db.Query(test.query)
if assert.NoError(t, err, "Query failed: %s", test.query) {
defer rows.Close()
cols, err := rows.ColumnTypes()
if assert.NoError(t, err, "ColumnTypes failed:%s", test.query) {
actual := calcColumnDetails(cols, test.fixed, test.variable)
actual, max := calcColumnDetails(cols, test.fixed, test.variable)
for i, a := range actual {
if test.details[i].displayWidth != a.displayWidth ||
test.details[i].leftJustify != a.leftJustify ||
test.details[i].zeroesAfterDecimal != a.zeroesAfterDecimal {
assert.Failf(t, "", "Incorrect test details for column [%s] in query '%s':%+v", cols[i].Name(), test.query, a)
assert.Failf(t, "", "[%d] Incorrect test details for column [%s] in query '%s':%+v", x, cols[i].Name(), test.query, a)
}
assert.Equal(t, test.max, max, "[%d] Max column name length incorrect", x)
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ func TestGetRunnableQuery(t *testing.T) {

func TestExitInitialQuery(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.Query = "EXIT(SELECT '1200', 2100)"
err := s.Run(true, false)
if assert.NoError(t, err, "s.Run(once = true)") {
Expand Down Expand Up @@ -240,6 +241,7 @@ func TestExitCodeSetOnError(t *testing.T) {

func TestSqlCmdExitOnError(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.Connect.ExitOnError = true
err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"})
o := buf.buf.String()
Expand All @@ -248,6 +250,7 @@ func TestSqlCmdExitOnError(t *testing.T) {
assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error")

s, buf = setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.Connect.ExitOnError = true
s.Connect.ErrorSeverityLevel = 15
s.vars.Set(SQLCMDERRORLEVEL, "14")
Expand Down Expand Up @@ -353,6 +356,46 @@ func TestPromptForPasswordPositive(t *testing.T) {
}
}

func TestVerticalLayoutNoColumns(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.vars.Set(SQLCMDFORMAT, "vert")
_, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300")
assert.NoError(t, err, "runQuery failed")
assert.Equal(t,
"100"+SqlcmdEol+"2000"+SqlcmdEol+"300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol,
buf.buf.String(), "Query without column headers")
}

func TestSelectGuidColumn(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
_, err := s.runQuery("select convert(uniqueidentifier, N'3ddba21e-ff0f-4d24-90b4-f355864d7865')")
assert.NoError(t, err, "runQuery failed")
assert.Equal(t, "3ddba21e-ff0f-4d24-90b4-f355864d7865"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a uniqueidentifier should work")
}

func TestSelectNullGuidColumn(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
_, err := s.runQuery("select convert(uniqueidentifier,null)")
assert.NoError(t, err, "runQuery failed")
assert.Equal(t, "NULL"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a null uniqueidentifier should work")
}

func TestVerticalLayoutWithColumns(t *testing.T) {
s, buf := setupSqlCmdWithMemoryOutput(t)
defer buf.Close()
s.vars.Set(SQLCMDFORMAT, "vert")
s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "256")
_, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300")
assert.NoError(t, err, "runQuery failed")
assert.Equal(t,
"column1 100"+SqlcmdEol+"col2 2000"+SqlcmdEol+" 300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol,
buf.buf.String(), "Query without column headers")

}

// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input
func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error {
t.Helper()
Expand Down
11 changes: 11 additions & 0 deletions pkg/sqlcmd/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
SQLCMDCOLSEP = "SQLCMDCOLSEP"
SQLCMDCOLWIDTH = "SQLCMDCOLWIDTH"
SQLCMDERRORLEVEL = "SQLCMDERRORLEVEL"
SQLCMDFORMAT = "SQLCMDFORMAT"
SQLCMDMAXVARTYPEWIDTH = "SQLCMDMAXVARTYPEWIDTH"
SQLCMDMAXFIXEDTYPEWIDTH = "SQLCMDMAXFIXEDTYPEWIDTH"
SQLCMDEDITOR = "SQLCMDEDITOR"
Expand All @@ -41,6 +42,7 @@ var builtinVariables = []string{
SQLCMDDBNAME,
SQLCMDEDITOR,
SQLCMDERRORLEVEL,
SQLCMDFORMAT,
SQLCMDHEADERS,
SQLCMDINI,
SQLCMDLOGINTIMEOUT,
Expand Down Expand Up @@ -168,6 +170,15 @@ func (v Variables) ErrorLevel() int64 {
return mustValue(v[SQLCMDERRORLEVEL])
}

// Format is the name of the results format
func (v Variables) Format() string {
switch v[SQLCMDFORMAT] {
case "vert", "vertical":
return "vertical"
}
return "horizontal"
}

func mustValue(val string) int64 {
var n int64
_, err := fmt.Sscanf(val, "%d", &n)
Expand Down