From 7d15adf1d9602d8ab6013365511932ab7845b027 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 16:25:26 -0500 Subject: [PATCH 01/12] feat: implement -j raw-errors flag for ODBC sqlcmd compatibility Adds the -j / --raw-errors flag to the legacy sqlcmd CLI, mirroring the ODBC sqlcmd's m_PrintRawErrorMessages behavior (Sql/utils/sqlcmd/console/src/Formatter.cpp). When set, the driver-supplied 'mssql: ' prefix is preserved in error output; the Msg/Level/State/Server header and screen-width wrapping are unaffected, matching ODBC sqlcmd which also only gates the prefix strip. Closes the -j gap tracked in discussion #292. --- cmd/sqlcmd/sqlcmd.go | 4 +++- cmd/sqlcmd/sqlcmd_test.go | 3 +++ pkg/sqlcmd/format.go | 26 ++++++++++++++++++++++--- pkg/sqlcmd/format_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index b27ecb0b..2d751dae 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -83,6 +83,7 @@ type SQLCmdArguments struct { ChangePasswordAndExit string TraceFile string ServerNameOverride string + RawErrors bool // Keep Help at the end of the list Help bool Ascii bool @@ -487,6 +488,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) { 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)) rootCmd.Flags().IntVarP(&args.ErrorLevel, "error-level", "m", 0, localizer.Sprintf("Controls which error messages are sent to %s. Messages that have severity level greater than or equal to this level are sent", localizer.StdoutName)) + rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Prints raw error messages from the SQL Server driver without removing the driver-supplied prefix from the message text")) //Need to decide on short of Header , as "h" is already used in help command in Cobra rootCmd.Flags().IntVarP(&args.Headers, "headers", "h", 0, localizer.Sprintf("Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed")) @@ -871,7 +873,7 @@ func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { } s.Connect = &connectConfig - s.Format = sqlcmd.NewSQLCmdDefaultFormatter(vars, args.TrimSpaces, args.getControlCharacterBehavior()) + s.Format = sqlcmd.NewSQLCmdDefaultFormatter(vars, args.TrimSpaces, args.getControlCharacterBehavior(), sqlcmd.WithRawErrors(args.RawErrors)) if args.OutputFile != "" { err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile}) if err != nil { diff --git a/cmd/sqlcmd/sqlcmd_test.go b/cmd/sqlcmd/sqlcmd_test.go index 4554998a..b81b805b 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -123,6 +123,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) { {[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool { return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem" }}, + {[]string{"-j"}, func(args SQLCmdArguments) bool { + return args.RawErrors + }}, } for _, test := range commands { diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 71bb00e2..2ff31cc5 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -85,20 +85,38 @@ type sqlCmdFormatterType struct { maxColNameLen int colorizer color.Colorizer xml bool + rawErrors bool +} + +// FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. +type FormatterOption func(*sqlCmdFormatterType) + +// WithRawErrors controls the -j "raw error messages" behavior. When enabled, +// the driver-supplied error text is emitted verbatim (the "mssql: " prefix +// that go-mssqldb prepends is not stripped). The Msg/Level/State/Server +// header and screen-width wrapping are unaffected, matching the ODBC sqlcmd -j +// implementation, which also only suppresses driver-prefix stripping. +func WithRawErrors(raw bool) FormatterOption { + return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } // 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 { +// FormatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. +func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { if vars.Format() == "ascii" { return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) } - return &sqlCmdFormatterType{ + f := &sqlCmdFormatterType{ removeTrailingSpaces: removeTrailingSpaces, format: "horizontal", colorizer: color.New(false), ccb: ccb, } + for _, opt := range opts { + opt(f) + } + return f } // Adds the given string to the current line, wrapping it based on the screen width setting @@ -232,7 +250,9 @@ func (f *sqlCmdFormatterType) AddError(err error) { } else { b.WriteString(localizer.Sprintf("Msg %#v, Level %d, State %d, Server %s, Line %#v%s", e.Number, e.Class, e.State, e.ServerName, e.LineNo, SqlcmdEol)) } - msg = strings.TrimPrefix(msg, "mssql: ") + if !f.rawErrors { + msg = strings.TrimPrefix(msg, "mssql: ") + } } } if print { diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index f4bee464..1228d60a 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + mssql "github.com/microsoft/go-mssqldb" "github.com/microsoft/go-sqlcmd/internal/color" "github.com/stretchr/testify/assert" ) @@ -162,3 +163,43 @@ func TestFormatterXmlMode(t *testing.T) { assert.NoError(t, err, "runSqlCmd returned error") assert.Equal(t, ``+SqlcmdEol, buf.buf.String()) } + +func TestAddErrorRawErrors(t *testing.T) { + mssqlErr := mssql.Error{ + Number: 50000, + State: 1, + Class: 16, + Message: "Something failed", + ServerName: "server", + LineNo: 7, + } + + tests := []struct { + name string + rawErrors bool + }{ + {name: "default strips mssql prefix", rawErrors: false}, + {name: "raw preserves mssql prefix", rawErrors: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + out := new(strings.Builder) + errOut := new(strings.Builder) + vars := InitializeVariables(false) + f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(tc.rawErrors)) + f.BeginBatch("", vars, out, errOut) + + f.AddError(mssqlErr) + + got := errOut.String() + assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7", "header should always be printed") + assert.Contains(t, got, "Something failed", "message body should always be printed") + if tc.rawErrors { + assert.Contains(t, got, "mssql: Something failed", "raw mode preserves the driver-supplied prefix") + } else { + assert.NotContains(t, got, "mssql: ", "default mode strips the driver-supplied prefix") + } + }) + } +} From f6ac972a027734962828c817caaddac7ae8667ef Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 16:41:49 -0500 Subject: [PATCH 02/12] refactor: skip nil FormatterOption to harden variadic API --- pkg/sqlcmd/format.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 2ff31cc5..9b2de047 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -114,7 +114,9 @@ func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb C ccb: ccb, } for _, opt := range opts { - opt(f) + if opt != nil { + opt(f) + } } return f } From 7ec4ee63c4c2577ddfd4cdd409fe96a87227177a Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 16:43:40 -0500 Subject: [PATCH 03/12] docs: clarify -j wording to name the go-mssqldb prefix explicitly --- cmd/sqlcmd/sqlcmd.go | 2 +- pkg/sqlcmd/format.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index 2d751dae..c0e8d80b 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -488,7 +488,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) { 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)) rootCmd.Flags().IntVarP(&args.ErrorLevel, "error-level", "m", 0, localizer.Sprintf("Controls which error messages are sent to %s. Messages that have severity level greater than or equal to this level are sent", localizer.StdoutName)) - rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Prints raw error messages from the SQL Server driver without removing the driver-supplied prefix from the message text")) + rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Prints error messages exactly as returned by the go-mssqldb client driver, without stripping its \"mssql: \" prefix")) //Need to decide on short of Header , as "h" is already used in help command in Cobra rootCmd.Flags().IntVarP(&args.Headers, "headers", "h", 0, localizer.Sprintf("Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed")) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 9b2de047..798cae0d 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -92,10 +92,11 @@ type sqlCmdFormatterType struct { type FormatterOption func(*sqlCmdFormatterType) // WithRawErrors controls the -j "raw error messages" behavior. When enabled, -// the driver-supplied error text is emitted verbatim (the "mssql: " prefix -// that go-mssqldb prepends is not stripped). The Msg/Level/State/Server -// header and screen-width wrapping are unaffected, matching the ODBC sqlcmd -j -// implementation, which also only suppresses driver-prefix stripping. +// the error string returned by the go-mssqldb client driver is emitted verbatim +// (the "mssql: " prefix that go-mssqldb's mssql.Error.Error() prepends is not +// stripped). The Msg/Level/State/Server header and screen-width wrapping are +// unaffected, matching the ODBC sqlcmd -j implementation, which likewise only +// suppresses the prefix strip. func WithRawErrors(raw bool) FormatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } From af71e7cd2369fe9e598525dd224d91982541a0f5 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 16:58:48 -0500 Subject: [PATCH 04/12] refactor: trim -j docs and split test for DAMP readability --- cmd/sqlcmd/sqlcmd.go | 2 +- pkg/sqlcmd/format.go | 8 ++---- pkg/sqlcmd/format_test.go | 59 +++++++++++++++------------------------ 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/cmd/sqlcmd/sqlcmd.go b/cmd/sqlcmd/sqlcmd.go index c0e8d80b..1d4d42bd 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -488,7 +488,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) { 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)) rootCmd.Flags().IntVarP(&args.ErrorLevel, "error-level", "m", 0, localizer.Sprintf("Controls which error messages are sent to %s. Messages that have severity level greater than or equal to this level are sent", localizer.StdoutName)) - rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Prints error messages exactly as returned by the go-mssqldb client driver, without stripping its \"mssql: \" prefix")) + rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Keep the \"mssql: \" prefix that go-mssqldb adds to error messages")) //Need to decide on short of Header , as "h" is already used in help command in Cobra rootCmd.Flags().IntVarP(&args.Headers, "headers", "h", 0, localizer.Sprintf("Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed")) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 798cae0d..185ca89c 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -91,12 +91,8 @@ type sqlCmdFormatterType struct { // FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. type FormatterOption func(*sqlCmdFormatterType) -// WithRawErrors controls the -j "raw error messages" behavior. When enabled, -// the error string returned by the go-mssqldb client driver is emitted verbatim -// (the "mssql: " prefix that go-mssqldb's mssql.Error.Error() prepends is not -// stripped). The Msg/Level/State/Server header and screen-width wrapping are -// unaffected, matching the ODBC sqlcmd -j implementation, which likewise only -// suppresses the prefix strip. +// WithRawErrors implements -j: when raw is true, AddError keeps the "mssql: " +// prefix that go-mssqldb adds to error text instead of stripping it. func WithRawErrors(raw bool) FormatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 1228d60a..0381d96d 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -164,42 +164,29 @@ func TestFormatterXmlMode(t *testing.T) { assert.Equal(t, ``+SqlcmdEol, buf.buf.String()) } -func TestAddErrorRawErrors(t *testing.T) { - mssqlErr := mssql.Error{ - Number: 50000, - State: 1, - Class: 16, - Message: "Something failed", - ServerName: "server", - LineNo: 7, - } +func TestAddErrorStripsMssqlPrefixByDefault(t *testing.T) { + out, errOut := new(strings.Builder), new(strings.Builder) + vars := InitializeVariables(false) + f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore) + f.BeginBatch("", vars, out, errOut) + + f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7}) + + got := errOut.String() + assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7") + assert.Contains(t, got, "Something failed") + assert.NotContains(t, got, "mssql: Something failed") +} - tests := []struct { - name string - rawErrors bool - }{ - {name: "default strips mssql prefix", rawErrors: false}, - {name: "raw preserves mssql prefix", rawErrors: true}, - } +func TestAddErrorWithRawErrorsKeepsMssqlPrefix(t *testing.T) { + out, errOut := new(strings.Builder), new(strings.Builder) + vars := InitializeVariables(false) + f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(true)) + f.BeginBatch("", vars, out, errOut) - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - out := new(strings.Builder) - errOut := new(strings.Builder) - vars := InitializeVariables(false) - f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(tc.rawErrors)) - f.BeginBatch("", vars, out, errOut) - - f.AddError(mssqlErr) - - got := errOut.String() - assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7", "header should always be printed") - assert.Contains(t, got, "Something failed", "message body should always be printed") - if tc.rawErrors { - assert.Contains(t, got, "mssql: Something failed", "raw mode preserves the driver-supplied prefix") - } else { - assert.NotContains(t, got, "mssql: ", "default mode strips the driver-supplied prefix") - } - }) - } + f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7}) + + got := errOut.String() + assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7") + assert.Contains(t, got, "mssql: Something failed") } From 92b373f79c27ff822b683bc3f33075a5ac998709 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 17:15:52 -0500 Subject: [PATCH 05/12] docs: drop driver name from -j help text --- 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 1d4d42bd..66ca9af6 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -488,7 +488,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) { 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)) rootCmd.Flags().IntVarP(&args.ErrorLevel, "error-level", "m", 0, localizer.Sprintf("Controls which error messages are sent to %s. Messages that have severity level greater than or equal to this level are sent", localizer.StdoutName)) - rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Keep the \"mssql: \" prefix that go-mssqldb adds to error messages")) + rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Do not strip the \"mssql: \" prefix from error messages")) //Need to decide on short of Header , as "h" is already used in help command in Cobra rootCmd.Flags().IntVarP(&args.Headers, "headers", "h", 0, localizer.Sprintf("Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed")) From 1b703f69d03479dcbbb4e6062e5b09f6114ba7d1 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 17:32:49 -0500 Subject: [PATCH 06/12] refactor: unexport formatterOption for closed-set options --- pkg/sqlcmd/format.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 185ca89c..d74b6d7c 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -88,19 +88,20 @@ type sqlCmdFormatterType struct { rawErrors bool } -// FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. -type FormatterOption func(*sqlCmdFormatterType) +// formatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. +// The type is unexported so options must come from helpers in this package. +type formatterOption func(*sqlCmdFormatterType) // WithRawErrors implements -j: when raw is true, AddError keeps the "mssql: " // prefix that go-mssqldb adds to error text instead of stripping it. -func WithRawErrors(raw bool) FormatterOption { +func WithRawErrors(raw bool) formatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } // 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. -// FormatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. -func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { +// formatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. +func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...formatterOption) Formatter { if vars.Format() == "ascii" { return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) } From d488f5fcc04069037518562b4ac2032e7f7de875 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 17:48:11 -0500 Subject: [PATCH 07/12] refactor: re-export FormatterOption for public SDK consumers --- pkg/sqlcmd/format.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index d74b6d7c..185ca89c 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -88,20 +88,19 @@ type sqlCmdFormatterType struct { rawErrors bool } -// formatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. -// The type is unexported so options must come from helpers in this package. -type formatterOption func(*sqlCmdFormatterType) +// FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. +type FormatterOption func(*sqlCmdFormatterType) // WithRawErrors implements -j: when raw is true, AddError keeps the "mssql: " // prefix that go-mssqldb adds to error text instead of stripping it. -func WithRawErrors(raw bool) formatterOption { +func WithRawErrors(raw bool) FormatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } // 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. -// formatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. -func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...formatterOption) Formatter { +// FormatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. +func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { if vars.Format() == "ascii" { return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) } From 3bcbf4bd25b8f43954950d89b8d6a3e8b65f1e48 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 18:00:35 -0500 Subject: [PATCH 08/12] test: tighten default-mode assertion to forbid any mssql: occurrence --- pkg/sqlcmd/format_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 0381d96d..064a92d8 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -175,7 +175,7 @@ func TestAddErrorStripsMssqlPrefixByDefault(t *testing.T) { got := errOut.String() assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7") assert.Contains(t, got, "Something failed") - assert.NotContains(t, got, "mssql: Something failed") + assert.NotContains(t, got, "mssql:") } func TestAddErrorWithRawErrorsKeepsMssqlPrefix(t *testing.T) { From 23595cd13434af50898624bf149b38ad464d4881 Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 18:16:29 -0500 Subject: [PATCH 09/12] docs: decouple WithRawErrors doc from CLI flag name --- pkg/sqlcmd/format.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 185ca89c..f899c60e 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -91,8 +91,9 @@ type sqlCmdFormatterType struct { // FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. type FormatterOption func(*sqlCmdFormatterType) -// WithRawErrors implements -j: when raw is true, AddError keeps the "mssql: " -// prefix that go-mssqldb adds to error text instead of stripping it. +// WithRawErrors controls AddError prefix handling: when raw is true, AddError +// keeps the "mssql: " prefix that go-mssqldb adds to error text instead of +// stripping it. func WithRawErrors(raw bool) FormatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } From 3b47c97cdfdfd47742c5e9c830397f552eeaf97c Mon Sep 17 00:00:00 2001 From: David Levy Date: Tue, 19 May 2026 18:26:51 -0500 Subject: [PATCH 10/12] docs: document FormatterOption and opts param; test: add --raw-errors long-form case --- cmd/sqlcmd/sqlcmd_test.go | 3 +++ pkg/sqlcmd/format.go | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/sqlcmd/sqlcmd_test.go b/cmd/sqlcmd/sqlcmd_test.go index b81b805b..5f6f2740 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -126,6 +126,9 @@ func TestValidCommandLineToArgsConversion(t *testing.T) { {[]string{"-j"}, func(args SQLCmdArguments) bool { return args.RawErrors }}, + {[]string{"--raw-errors"}, func(args SQLCmdArguments) bool { + return args.RawErrors + }}, } for _, test := range commands { diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index f899c60e..a25ad2ab 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -88,7 +88,8 @@ type sqlCmdFormatterType struct { rawErrors bool } -// FormatterOption configures a Formatter returned by NewSQLCmdDefaultFormatter. +// FormatterOption customizes the default formatter built by NewSQLCmdDefaultFormatter. +// Use the provided With* constructors (e.g. WithRawErrors) to supply options. type FormatterOption func(*sqlCmdFormatterType) // WithRawErrors controls AddError prefix handling: when raw is true, AddError @@ -100,7 +101,7 @@ func WithRawErrors(raw bool) FormatterOption { // 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. -// FormatterOption values (e.g. WithRawErrors) apply only to the ODBC-mimicking formatter; the ASCII formatter ignores them. +// Any FormatterOption values passed via opts (e.g. WithRawErrors) are applied to the ODBC-mimicking formatter; the ASCII formatter ignores them. func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { if vars.Format() == "ascii" { return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) From ac96fce68aeccfaeef53a66f53c81773a396f819 Mon Sep 17 00:00:00 2001 From: David Levy Date: Wed, 27 May 2026 10:01:27 -0500 Subject: [PATCH 11/12] fix: apply FormatterOption values on the ascii result-format path NewSQLCmdDefaultFormatter previously returned the ascii formatter without applying caller-supplied FormatterOption values, so -j raw-errors silently did nothing when the ascii output format was selected. Apply options to the embedded sqlCmdFormatterType in both branches and add a regression test. --- pkg/sqlcmd/format.go | 12 +++++++++--- pkg/sqlcmd/format_test.go | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index a25ad2ab..80c2082b 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -101,10 +101,12 @@ func WithRawErrors(raw bool) FormatterOption { // 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. -// Any FormatterOption values passed via opts (e.g. WithRawErrors) are applied to the ODBC-mimicking formatter; the ASCII formatter ignores them. +// Any FormatterOption values (e.g. WithRawErrors) are applied to the returned formatter. func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { if vars.Format() == "ascii" { - return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb) + f := NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb).(*asciiFormatter) + applyFormatterOptions(f.sqlCmdFormatterType, opts) + return f } f := &sqlCmdFormatterType{ removeTrailingSpaces: removeTrailingSpaces, @@ -112,12 +114,16 @@ func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb C colorizer: color.New(false), ccb: ccb, } + applyFormatterOptions(f, opts) + return f +} + +func applyFormatterOptions(f *sqlCmdFormatterType, opts []FormatterOption) { for _, opt := range opts { if opt != nil { opt(f) } } - return f } // Adds the given string to the current line, wrapping it based on the screen width setting diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 064a92d8..f32f545c 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -190,3 +190,17 @@ func TestAddErrorWithRawErrorsKeepsMssqlPrefix(t *testing.T) { assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7") assert.Contains(t, got, "mssql: Something failed") } + +// TestAddErrorWithRawErrorsAppliesToAsciiFormatter guards against silently dropping +// FormatterOption values when the caller has selected the ascii result format. +func TestAddErrorWithRawErrorsAppliesToAsciiFormatter(t *testing.T) { + out, errOut := new(strings.Builder), new(strings.Builder) + vars := InitializeVariables(false) + vars.Set(SQLCMDFORMAT, "ascii") + f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(true)) + f.BeginBatch("", vars, out, errOut) + + f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7}) + + assert.Contains(t, errOut.String(), "mssql: Something failed", "ascii formatter must honor WithRawErrors") +} From 6e965e75586fbdb74b68e9eabb1208934519a260 Mon Sep 17 00:00:00 2001 From: David Levy Date: Wed, 27 May 2026 10:19:09 -0500 Subject: [PATCH 12/12] docs: trim public formatter godoc to drop restated behavior Remove three slop lines: the FormatterOption hand-holding sentence, the NewSQLCmdDefaultFormatter line that restates how variadic options work, and a non-conforming test header comment (no other test in the file carries one). Tighten WithRawErrors to a single sentence. --- pkg/sqlcmd/format.go | 13 +++++-------- pkg/sqlcmd/format_test.go | 2 -- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 80c2082b..3ffea3a8 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -88,20 +88,17 @@ type sqlCmdFormatterType struct { rawErrors bool } -// FormatterOption customizes the default formatter built by NewSQLCmdDefaultFormatter. -// Use the provided With* constructors (e.g. WithRawErrors) to supply options. +// FormatterOption customizes the formatter returned by NewSQLCmdDefaultFormatter. type FormatterOption func(*sqlCmdFormatterType) -// WithRawErrors controls AddError prefix handling: when raw is true, AddError -// keeps the "mssql: " prefix that go-mssqldb adds to error text instead of -// stripping it. +// WithRawErrors makes AddError preserve the "mssql: " prefix that go-mssqldb +// adds to error text instead of stripping it. func WithRawErrors(raw bool) FormatterOption { return func(f *sqlCmdFormatterType) { f.rawErrors = raw } } -// 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. -// Any FormatterOption values (e.g. WithRawErrors) are applied to the returned formatter. +// NewSQLCmdDefaultFormatter returns an ASCII formatter when SQLCMDFORMAT is "ascii", +// otherwise a formatter that mimics the original ODBC-based sqlcmd formatter. func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter { if vars.Format() == "ascii" { f := NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb).(*asciiFormatter) diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index f32f545c..d0e085ff 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -191,8 +191,6 @@ func TestAddErrorWithRawErrorsKeepsMssqlPrefix(t *testing.T) { assert.Contains(t, got, "mssql: Something failed") } -// TestAddErrorWithRawErrorsAppliesToAsciiFormatter guards against silently dropping -// FormatterOption values when the caller has selected the ascii result format. func TestAddErrorWithRawErrorsAppliesToAsciiFormatter(t *testing.T) { out, errOut := new(strings.Builder), new(strings.Builder) vars := InitializeVariables(false)