From 981bcdb9d69ac99930b32ded54ee1e557b6824b6 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Mon, 24 Nov 2025 01:54:50 +0800 Subject: [PATCH 01/17] Add ASCII table output format --- build/buildfast.cmd | 15 +++ cmd/sqlcmd/sqlcmd.go | 4 +- internal/sql/mssql.go | 2 +- pkg/sqlcmd/commands_test.go | 2 +- pkg/sqlcmd/format.go | 5 +- pkg/sqlcmd/format_ascii.go | 178 ++++++++++++++++++++++++++++++++++++ pkg/sqlcmd/sqlcmd_test.go | 10 +- pkg/sqlcmd/variables.go | 7 +- 8 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 build/buildfast.cmd create mode 100644 pkg/sqlcmd/format_ascii.go diff --git a/build/buildfast.cmd b/build/buildfast.cmd new file mode 100644 index 00000000..fbd15321 --- /dev/null +++ b/build/buildfast.cmd @@ -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 diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 5abc0860..9e17c257 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -713,7 +713,7 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) { if a.Vertical { return "vert" } - return "horizontal" + return "" }, } for varname, set := range varmap { @@ -862,7 +862,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 { diff --git a/internal/sql/mssql.go b/internal/sql/mssql.go index 442e514a..961846cf 100644 --- a/internal/sql/mssql.go +++ b/internal/sql/mssql.go @@ -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", diff --git a/pkg/sqlcmd/commands_test.go b/pkg/sqlcmd/commands_test.go index 56d509da..76c509a8 100644 --- a/pkg/sqlcmd/commands_test.go +++ b/pkg/sqlcmd/commands_test.go @@ -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)} diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 55bd2e25..569fbb0f 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -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", diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go new file mode 100644 index 00000000..8462a108 --- /dev/null +++ b/pkg/sqlcmd/format_ascii.go @@ -0,0 +1,178 @@ +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 + f.writeOut(SqlcmdEol, color.TextTypeNormal) +} + +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 { + 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 + if startCol < len(colWidths) { + f.writeOut(SqlcmdEol, color.TextTypeNormal) + } + } + } +} + +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 +} diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index ade6dd8c..6eda8880 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -635,11 +635,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 } @@ -649,7 +651,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) @@ -667,7 +669,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") diff --git a/pkg/sqlcmd/variables.go b/pkg/sqlcmd/variables.go index aa601627..0ba1a224 100644 --- a/pkg/sqlcmd/variables.go +++ b/pkg/sqlcmd/variables.go @@ -179,8 +179,12 @@ func (v Variables) Format() string { switch v[SQLCMDFORMAT] { case "vert", "vertical": return "vertical" + case "ascii": + return "ascii" + case "horiz", "horizontal": + return "horizontal" } - return "horizontal" + return "ascii" } // StartupScriptFile is the path to the file that contains the startup script @@ -246,6 +250,7 @@ func InitializeVariables(fromEnvironment bool) *Variables { SQLCMDUSER: "", SQLCMDUSEAAD: "", SQLCMDCOLORSCHEME: "", + SQLCMDFORMAT: "", } hostname, _ := os.Hostname() variables.Set(SQLCMDWORKSTATION, hostname) From 3662b54150371dae1a1ce27e7f7cf1f5684dba96 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Mon, 24 Nov 2025 02:38:05 +0800 Subject: [PATCH 02/17] Add test for ascii output --- pkg/sqlcmd/format_ascii.go | 4 --- pkg/sqlcmd/format_ascii_test.go | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 pkg/sqlcmd/format_ascii_test.go diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go index 8462a108..2b920d45 100644 --- a/pkg/sqlcmd/format_ascii.go +++ b/pkg/sqlcmd/format_ascii.go @@ -50,7 +50,6 @@ func (f *asciiFormatter) EndResultSet() { f.printAsciiTable() } f.rows = nil - f.writeOut(SqlcmdEol, color.TextTypeNormal) } func (f *asciiFormatter) printAsciiTable() { @@ -107,9 +106,6 @@ func (f *asciiFormatter) printAsciiTable() { f.printTableSegment(colWidths, startCol, endCol-1) startCol = endCol - if startCol < len(colWidths) { - f.writeOut(SqlcmdEol, color.TextTypeNormal) - } } } } diff --git a/pkg/sqlcmd/format_ascii_test.go b/pkg/sqlcmd/format_ascii_test.go new file mode 100644 index 00000000..3d91be72 --- /dev/null +++ b/pkg/sqlcmd/format_ascii_test.go @@ -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) +} From e827adbc3f0b3891f1de0ebf6df039cf56508cfe Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Mon, 24 Nov 2025 02:58:41 +0800 Subject: [PATCH 03/17] Add help text to disable ASCII table output format --- cmd/sqlcmd/sqlcmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 9e17c257..4aa7f6f2 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -464,7 +464,7 @@ 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. To disable ASCII table output format, use -v SQLCMDFORMAT=horizontal")) 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)) From ff2f94f0940d86fe829d9c0f390935253c4e0c7f Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Mon, 24 Nov 2025 07:56:25 +0800 Subject: [PATCH 04/17] Add ascii parameter. Disable ascii output format by default --- README.md | 1 + cmd/sqlcmd/sqlcmd.go | 11 +++++++++-- pkg/sqlcmd/variables.go | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 576439da..9d2e101a 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,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 diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 4aa7f6f2..bfefb4ef 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -84,7 +84,8 @@ type SQLCmdArguments struct { TraceFile string ServerNameOverride string // Keep Help at the end of the list - Help bool + Help bool + Ascii bool } func (args *SQLCmdArguments) useEnvVars() bool { @@ -464,7 +465,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. To disable ASCII table output format, use -v SQLCMDFORMAT=horizontal")) + 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)) @@ -477,6 +479,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)) @@ -713,6 +717,9 @@ func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) { if a.Vertical { return "vert" } + if a.Ascii { + return "ascii" + } return "" }, } diff --git a/pkg/sqlcmd/variables.go b/pkg/sqlcmd/variables.go index 0ba1a224..d4f7fa7f 100644 --- a/pkg/sqlcmd/variables.go +++ b/pkg/sqlcmd/variables.go @@ -184,7 +184,7 @@ func (v Variables) Format() string { case "horiz", "horizontal": return "horizontal" } - return "ascii" + return "horizontal" } // StartupScriptFile is the path to the file that contains the startup script From b98ddbc7dc6fa7fccd1372b2962a20e515cc7773 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:04:40 +0800 Subject: [PATCH 05/17] Mark --vertical and --ascii as mutually exclusive --- cmd/sqlcmd/sqlcmd.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index bfefb4ef..60b3878e 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -152,6 +152,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) { switch { case len(a.InputFile) > 0 && (len(a.Query) > 0 || len(a.InitialQuery) > 0): err = mutuallyExclusiveError("i", `-Q/-q`) + case a.Vertical && a.Ascii: + err = mutuallyExclusiveError("--vertical", "--ascii") case a.UseTrustedConnection && (len(a.UserName) > 0 || len(a.Password) > 0): err = mutuallyExclusiveError("-E", `-U/-P`) case a.UseAad && len(a.AuthenticationMethod) > 0: From b0b372f5d0293576d1d5c08ff74cb191b789bce3 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:07:48 +0800 Subject: [PATCH 06/17] Iterated over all rows just once, and some other small fixes --- pkg/sqlcmd/format_ascii.go | 70 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go index 2b920d45..a8004d06 100644 --- a/pkg/sqlcmd/format_ascii.go +++ b/pkg/sqlcmd/format_ascii.go @@ -12,7 +12,8 @@ import ( type asciiFormatter struct { *sqlCmdFormatterType - rows [][]string + rows [][]string + colWidths []int } func NewSQLCmdAsciiFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter { @@ -30,6 +31,10 @@ func NewSQLCmdAsciiFormatter(vars *Variables, removeTrailingSpaces bool, ccb Con func (f *asciiFormatter) BeginResultSet(cols []*sql.ColumnType) { f.sqlCmdFormatterType.BeginResultSet(cols) f.rows = make([][]string, 0) + f.colWidths = make([]int, len(f.columnDetails)) + for i, c := range f.columnDetails { + f.colWidths[i] = utf8.RuneCountInString(c.col.Name()) + } } func (f *asciiFormatter) AddRow(row *sql.Rows) string { @@ -39,6 +44,14 @@ func (f *asciiFormatter) AddRow(row *sql.Rows) string { return "" } f.rows = append(f.rows, values) + for i, val := range values { + if i < len(f.colWidths) { + l := utf8.RuneCountInString(val) + if l > f.colWidths[i] { + f.colWidths[i] = l + } + } + } if len(values) > 0 { return values[0] } @@ -50,26 +63,10 @@ func (f *asciiFormatter) EndResultSet() { f.printAsciiTable() } f.rows = nil + f.colWidths = 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 { - 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 { @@ -80,19 +77,19 @@ func (f *asciiFormatter) printAsciiTable() { } totalWidth := 1 - for _, w := range colWidths { + for _, w := range f.colWidths { totalWidth += w + 3 } if totalWidth <= maxWidth { - f.printTableSegment(colWidths, 0, len(colWidths)-1) + f.printTableSegment(f.colWidths, 0, len(f.colWidths)-1) } else { startCol := 0 - for startCol < len(colWidths) { + for startCol < len(f.colWidths) { currentWidth := 1 endCol := startCol - for endCol < len(colWidths) { - w := colWidths[endCol] + 3 + for endCol < len(f.colWidths) { + w := f.colWidths[endCol] + 3 if currentWidth+w > maxWidth { break } @@ -104,7 +101,7 @@ func (f *asciiFormatter) printAsciiTable() { endCol++ } - f.printTableSegment(colWidths, startCol, endCol-1) + f.printTableSegment(f.colWidths, startCol, endCol-1) startCol = endCol } } @@ -115,22 +112,27 @@ func (f *asciiFormatter) printTableSegment(colWidths []int, startCol, endCol int return } + sep := f.vars.ColumnSeparator() + if sep == "" || sep == " " { + sep = "|" + } + divider := "+" for i := startCol; i <= endCol; i++ { divider += strings.Repeat("-", colWidths[i]+2) + "+" } - f.writeOut(divider+SqlcmdEol, color.TextTypeNormal) + f.writeOut(divider+SqlcmdEol, color.TextTypeSeparator) - header := "|" + header := sep for i := startCol; i <= endCol; i++ { name := f.columnDetails[i].col.Name() - header += " " + padRightString(name, colWidths[i]) + " |" + header += " " + padRightString(name, colWidths[i]) + " " + sep } - f.writeOut(header+SqlcmdEol, color.TextTypeNormal) - f.writeOut(divider+SqlcmdEol, color.TextTypeNormal) + f.writeOut(header+SqlcmdEol, color.TextTypeHeader) + f.writeOut(divider+SqlcmdEol, color.TextTypeSeparator) for _, row := range f.rows { - line := "|" + line := sep for i := startCol; i <= endCol; i++ { val := "" if i < len(row) { @@ -139,14 +141,14 @@ func (f *asciiFormatter) printTableSegment(colWidths []int, startCol, endCol int isNumeric := isNumericType(f.columnDetails[i].col.DatabaseTypeName()) if isNumeric { - line += " " + padLeftString(val, colWidths[i]) + " |" + line += " " + padLeftString(val, colWidths[i]) + " " + sep } else { - line += " " + padRightString(val, colWidths[i]) + " |" + line += " " + padRightString(val, colWidths[i]) + " " + sep } } - f.writeOut(line+SqlcmdEol, color.TextTypeNormal) + f.writeOut(line+SqlcmdEol, color.TextTypeCell) } - f.writeOut(divider+SqlcmdEol, color.TextTypeNormal) + f.writeOut(divider+SqlcmdEol, color.TextTypeSeparator) } func padRightString(s string, width int) string { From 589d0c6890d6036aa6756e138f97279e65118865 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:11:55 +0800 Subject: [PATCH 07/17] Follow the instruction convention by removing ending period --- cmd/sqlcmd/sqlcmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 60b3878e..b27ecb0b 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -467,7 +467,7 @@ 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)) From 1c2547df4a22fa0bae1c2ab1e5100b064f4cb041 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:21:58 +0800 Subject: [PATCH 08/17] Update the document of the function NewSQLCmdDefaultFormatter --- pkg/sqlcmd/format.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 569fbb0f..71bb00e2 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -87,7 +87,8 @@ type sqlCmdFormatterType struct { xml bool } -// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter +// NewSQLCmdDefaultFormatter returns a Formatter based on the configuration. +// It returns an ASCII formatter if the format is set to "ascii", otherwise it returns a formatter that mimics the original ODBC-based sqlcmd formatter. func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter { if vars.Format() == "ascii" { return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) From 8486756e521168bf80a503c56c51cdce538c5e24 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:29:41 +0800 Subject: [PATCH 09/17] Enhance ASCII table format output documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d2e101a..7c40cfa1 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,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`). +- 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`). Note that when using the ASCII table format, the `SQLCMDCOLWIDTH` variable and the `-w` parameter are ignored, as the table width is determined by the content. ``` 1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid From 372b238ea6321bdfc64b5de6142baf25d960881b Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 25 Nov 2025 02:31:27 +0800 Subject: [PATCH 10/17] Remove temp build script --- build/buildfast.cmd | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 build/buildfast.cmd diff --git a/build/buildfast.cmd b/build/buildfast.cmd deleted file mode 100644 index fbd15321..00000000 --- a/build/buildfast.cmd +++ /dev/null @@ -1,15 +0,0 @@ -@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 From a11e9b63a9e2756bede791255325e91eff50a7b8 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Sat, 31 Jan 2026 18:25:24 +0800 Subject: [PATCH 11/17] Update README to correctly document which variables are used vs ignored when using ASCII table format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c40cfa1..d1eab395 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,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`). Note that when using the ASCII table format, the `SQLCMDCOLWIDTH` variable and the `-w` parameter are ignored, as the table width is determined by the content. +- 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`). Note that when using the ASCII table format, individual column widths are determined by the content, but the `SQLCMDCOLWIDTH` variable and the `-w` parameter are still used to control the maximum screen width, determining when columns wrap into separate table segments. The following variables are ignored: `SQLCMDMAXFIXEDTYPEWIDTH`, `SQLCMDMAXVARTYPEWIDTH`, and `SQLCMDHEADERS`. ``` 1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid From 480f369ce84222a51bfe0f7a291a16c881b48540 Mon Sep 17 00:00:00 2001 From: David Levy Date: Mon, 11 May 2026 18:42:00 -0500 Subject: [PATCH 12/17] Refactor buffer closure in ASCII format tests Refactor deferred buffer closing to assert no error. --- pkg/sqlcmd/format_ascii_test.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/sqlcmd/format_ascii_test.go b/pkg/sqlcmd/format_ascii_test.go index 3d91be72..d0e40d58 100644 --- a/pkg/sqlcmd/format_ascii_test.go +++ b/pkg/sqlcmd/format_ascii_test.go @@ -11,22 +11,24 @@ func TestAsciiFormatter(t *testing.T) { if s.db == nil { t.Skip("No database connection available") } - defer buf.Close() - + defer func() { + assert.NoError(t, 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()) } @@ -35,29 +37,30 @@ func TestAsciiFormatterWrapping(t *testing.T) { if s.db == nil { t.Skip("No database connection available") } - defer buf.Close() - + defer func() { + assert.NoError(t, 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) } From 6bb154d70b30f94c056c6396aec6257e539041e9 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Tue, 12 May 2026 17:58:55 +0800 Subject: [PATCH 13/17] Truncate wide column --- pkg/sqlcmd/format_ascii.go | 29 +++++++-- pkg/sqlcmd/format_ascii_test.go | 104 ++++++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go index a8004d06..7211f657 100644 --- a/pkg/sqlcmd/format_ascii.go +++ b/pkg/sqlcmd/format_ascii.go @@ -76,6 +76,19 @@ func (f *asciiFormatter) printAsciiTable() { } } + // Limit column width to maxWidth - 4 (border + padding) + // 1 (left border) + 1 (space) + content + 1 (space) + 1 (right border) = content + 4 + maxColContentWidth := maxWidth - 4 + if maxColContentWidth < 1 { + maxColContentWidth = 1 + } + + for i := range f.colWidths { + if f.colWidths[i] > maxColContentWidth { + f.colWidths[i] = maxColContentWidth + } + } + totalWidth := 1 for _, w := range f.colWidths { totalWidth += w + 3 @@ -153,16 +166,24 @@ func (f *asciiFormatter) printTableSegment(colWidths []int, startCol, endCol int func padRightString(s string, width int) string { l := utf8.RuneCountInString(s) - if l >= width { - return s + if l > width { + r := []rune(s) + if width >= 3 { + return string(r[:width-3]) + "..." + } + return string(r[:width]) } return s + strings.Repeat(" ", width-l) } func padLeftString(s string, width int) string { l := utf8.RuneCountInString(s) - if l >= width { - return s + if l > width { + r := []rune(s) + if width >= 3 { + return string(r[:width-3]) + "..." + } + return string(r[:width]) } return strings.Repeat(" ", width-l) + s } diff --git a/pkg/sqlcmd/format_ascii_test.go b/pkg/sqlcmd/format_ascii_test.go index d0e40d58..ec7c89c3 100644 --- a/pkg/sqlcmd/format_ascii_test.go +++ b/pkg/sqlcmd/format_ascii_test.go @@ -1,35 +1,58 @@ package sqlcmd import ( + "bytes" + "database/sql" + "reflect" "testing" + "unsafe" + "github.com/microsoft/go-sqlcmd/internal/color" "github.com/stretchr/testify/assert" ) +func setColumnInfo(c *sql.ColumnType, name string, dbType string) { + v := reflect.ValueOf(c).Elem() + fName := v.FieldByName("name") + if fName.IsValid() { + reflect.NewAt(fName.Type(), unsafe.Pointer(fName.UnsafeAddr())).Elem().SetString(name) + } + fType := v.FieldByName("databaseType") + if fType.IsValid() { + reflect.NewAt(fType.Type(), unsafe.Pointer(fType.UnsafeAddr())).Elem().SetString(dbType) + } +} + func TestAsciiFormatter(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - if s.db == nil { - t.Skip("No database connection available") + vars := InitializeVariables(false) + vars.Set(SQLCMDFORMAT, "ascii") + + buf := new(bytes.Buffer) + f := &asciiFormatter{ + sqlCmdFormatterType: &sqlCmdFormatterType{ + vars: vars, + out: buf, + colorizer: color.New(false), + format: "ascii", + }, + rows: [][]string{{"1", "test"}}, + colWidths: []int{2, 4}, } - defer func() { - assert.NoError(t, buf.Close()) - }() - // Set format to ascii - s.vars.Set(SQLCMDFORMAT, "ascii") - s.Format = NewSQLCmdDefaultFormatter(s.vars, false, ControlIgnore) + // Mock column details + f.columnDetails = make([]columnDetail, 2) + setColumnInfo(&f.columnDetails[0].col, "id", "INT") + setColumnInfo(&f.columnDetails[1].col, "name", "VARCHAR") - err := runSqlCmd(t, s, []string{"select 1 as id, 'test' as name", "GO"}) - assert.NoError(t, err, "runSqlCmd returned error") + f.printAsciiTable() expected := `+----+------+` + SqlcmdEol + `| id | name |` + SqlcmdEol + `+----+------+` + SqlcmdEol + `| 1 | test |` + SqlcmdEol + - `+----+------+` + SqlcmdEol + - `(1 row affected)` + SqlcmdEol + `+----+------+` + SqlcmdEol - assert.Equal(t, expected, buf.buf.String()) + assert.Equal(t, expected, buf.String()) } func TestAsciiFormatterWrapping(t *testing.T) { @@ -37,14 +60,13 @@ func TestAsciiFormatterWrapping(t *testing.T) { if s.db == nil { t.Skip("No database connection available") } - defer func() { - assert.NoError(t, buf.Close()) - }() + 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") @@ -64,3 +86,51 @@ func TestAsciiFormatterWrapping(t *testing.T) { assert.Contains(t, buf.buf.String(), expectedPart1) assert.Contains(t, buf.buf.String(), expectedPart2) } + +func TestAsciiFormatterTruncation(t *testing.T) { + vars := InitializeVariables(false) + vars.Set(SQLCMDCOLWIDTH, "20") + + buf := new(bytes.Buffer) + f := &asciiFormatter{ + sqlCmdFormatterType: &sqlCmdFormatterType{ + vars: vars, + out: buf, + colorizer: color.New(false), + format: "ascii", + }, + rows: [][]string{{"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}}, // 50 chars + colWidths: []int{50}, + } + + // Mock column details with empty column type (defaults to non-numeric, empty name) + f.columnDetails = []columnDetail{{}} + + f.printAsciiTable() + + output := buf.String() + + // Expected behavior: + // maxWidth = 20 + // maxColContentWidth = 20 - 4 = 16 + // colWidths[0] should be clamped to 16 + // The value should be truncated to 13 chars + "..." = 16 chars total. + + // Divider: + followed by 16 dashes + 2 dashes (padding) + + + // Total width: 1 + 16 + 2 + 1 = 20 + // Divider line: +------------------+ + + // Header: | | (padded to 16) + // Since name is empty. + + // Value: | AAAAAAAAAAAAA... | (13 A's followed by ...) + + expectedDivider := "+------------------+" + expectedValue := "| AAAAAAAAAAAAA... |" + + assert.Contains(t, output, expectedDivider) + assert.Contains(t, output, expectedValue) + + // Verify it does NOT contain the full string + assert.NotContains(t, output, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") +} From 8db59ace3fd47128f99d47bd3cdf2baf9fdb32c0 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Wed, 13 May 2026 09:55:45 +0800 Subject: [PATCH 14/17] Add copyright headers --- pkg/sqlcmd/format_ascii.go | 3 +++ pkg/sqlcmd/format_ascii_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go index 7211f657..a34354b2 100644 --- a/pkg/sqlcmd/format_ascii.go +++ b/pkg/sqlcmd/format_ascii.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + package sqlcmd import ( diff --git a/pkg/sqlcmd/format_ascii_test.go b/pkg/sqlcmd/format_ascii_test.go index ec7c89c3..5fc8320c 100644 --- a/pkg/sqlcmd/format_ascii_test.go +++ b/pkg/sqlcmd/format_ascii_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + package sqlcmd import ( From e27c6b15908b3acd07cae5915d3f21febf7790d7 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Wed, 13 May 2026 09:57:33 +0800 Subject: [PATCH 15/17] Increase rowcount in asciiFormatter.AddRow --- pkg/sqlcmd/format_ascii.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/sqlcmd/format_ascii.go b/pkg/sqlcmd/format_ascii.go index a34354b2..5f3a5130 100644 --- a/pkg/sqlcmd/format_ascii.go +++ b/pkg/sqlcmd/format_ascii.go @@ -47,6 +47,7 @@ func (f *asciiFormatter) AddRow(row *sql.Rows) string { return "" } f.rows = append(f.rows, values) + f.rowcount++ for i, val := range values { if i < len(f.colWidths) { l := utf8.RuneCountInString(val) From 1936aef0cc40e7554aa82a907db21bca1354f896 Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Wed, 13 May 2026 09:59:57 +0800 Subject: [PATCH 16/17] Restore assert.NoError --- pkg/sqlcmd/sqlcmd_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index 6eda8880..2c325fed 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -639,9 +639,7 @@ func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) { buf := &memoryBuffer{buf: new(bytes.Buffer)} s.SetOutput(buf) err := s.ConnectDb(nil, true) - if err != nil { - t.Logf("ConnectDb failed: %v", err) - } + assert.NoError(t, err, "s.ConnectDB") return s, buf } From 9cc0e8391e6ed6fe23e642da75b2216c06f3fb3c Mon Sep 17 00:00:00 2001 From: Terry Yang Date: Wed, 13 May 2026 10:05:14 +0800 Subject: [PATCH 17/17] Refactor buffer closure --- pkg/sqlcmd/format_ascii_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/sqlcmd/format_ascii_test.go b/pkg/sqlcmd/format_ascii_test.go index 5fc8320c..c339fead 100644 --- a/pkg/sqlcmd/format_ascii_test.go +++ b/pkg/sqlcmd/format_ascii_test.go @@ -63,13 +63,14 @@ func TestAsciiFormatterWrapping(t *testing.T) { if s.db == nil { t.Skip("No database connection available") } - defer buf.Close() + defer func() { + assert.NoError(t, 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")