Skip to content

Commit 6879335

Browse files
feat: implement -j raw-errors flag for ODBC sqlcmd compatibility (#759)
1 parent d5eb4e7 commit 6879335

4 files changed

Lines changed: 79 additions & 7 deletions

File tree

cmd/sqlcmd/sqlcmd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type SQLCmdArguments struct {
8383
ChangePasswordAndExit string
8484
TraceFile string
8585
ServerNameOverride string
86+
RawErrors bool
8687
// Keep Help at the end of the list
8788
Help bool
8889
Ascii bool
@@ -487,6 +488,7 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
487488
rootCmd.Flags().IntVar(&args.DriverLoggingLevel, "driver-logging-level", 0, localizer.Sprintf("Level of mssql driver messages to print"))
488489
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))
489490
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))
491+
rootCmd.Flags().BoolVarP(&args.RawErrors, "raw-errors", "j", false, localizer.Sprintf("Do not strip the \"mssql: \" prefix from error messages"))
490492

491493
//Need to decide on short of Header , as "h" is already used in help command in Cobra
492494
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) {
871873
}
872874

873875
s.Connect = &connectConfig
874-
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(vars, args.TrimSpaces, args.getControlCharacterBehavior())
876+
s.Format = sqlcmd.NewSQLCmdDefaultFormatter(vars, args.TrimSpaces, args.getControlCharacterBehavior(), sqlcmd.WithRawErrors(args.RawErrors))
875877
if args.OutputFile != "" {
876878
err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile})
877879
if err != nil {

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
123123
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
124124
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
125125
}},
126+
{[]string{"-j"}, func(args SQLCmdArguments) bool {
127+
return args.RawErrors
128+
}},
129+
{[]string{"--raw-errors"}, func(args SQLCmdArguments) bool {
130+
return args.RawErrors
131+
}},
126132
}
127133

128134
for _, test := range commands {

pkg/sqlcmd/format.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,42 @@ type sqlCmdFormatterType struct {
8585
maxColNameLen int
8686
colorizer color.Colorizer
8787
xml bool
88+
rawErrors bool
8889
}
8990

90-
// NewSQLCmdDefaultFormatter returns a Formatter based on the configuration.
91-
// 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.
92-
func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter {
91+
// FormatterOption customizes the formatter returned by NewSQLCmdDefaultFormatter.
92+
type FormatterOption func(*sqlCmdFormatterType)
93+
94+
// WithRawErrors makes AddError preserve the "mssql: " prefix that go-mssqldb
95+
// adds to error text instead of stripping it.
96+
func WithRawErrors(raw bool) FormatterOption {
97+
return func(f *sqlCmdFormatterType) { f.rawErrors = raw }
98+
}
99+
100+
// NewSQLCmdDefaultFormatter returns an ASCII formatter when SQLCMDFORMAT is "ascii",
101+
// otherwise a formatter that mimics the original ODBC-based sqlcmd formatter.
102+
func NewSQLCmdDefaultFormatter(vars *Variables, removeTrailingSpaces bool, ccb ControlCharacterBehavior, opts ...FormatterOption) Formatter {
93103
if vars.Format() == "ascii" {
94-
return NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb)
104+
f := NewSQLCmdAsciiFormatter(vars, removeTrailingSpaces, ccb).(*asciiFormatter)
105+
applyFormatterOptions(f.sqlCmdFormatterType, opts)
106+
return f
95107
}
96-
return &sqlCmdFormatterType{
108+
f := &sqlCmdFormatterType{
97109
removeTrailingSpaces: removeTrailingSpaces,
98110
format: "horizontal",
99111
colorizer: color.New(false),
100112
ccb: ccb,
101113
}
114+
applyFormatterOptions(f, opts)
115+
return f
116+
}
117+
118+
func applyFormatterOptions(f *sqlCmdFormatterType, opts []FormatterOption) {
119+
for _, opt := range opts {
120+
if opt != nil {
121+
opt(f)
122+
}
123+
}
102124
}
103125

104126
// Adds the given string to the current line, wrapping it based on the screen width setting
@@ -232,7 +254,9 @@ func (f *sqlCmdFormatterType) AddError(err error) {
232254
} else {
233255
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))
234256
}
235-
msg = strings.TrimPrefix(msg, "mssql: ")
257+
if !f.rawErrors {
258+
msg = strings.TrimPrefix(msg, "mssql: ")
259+
}
236260
}
237261
}
238262
if print {

pkg/sqlcmd/format_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
mssql "github.com/microsoft/go-mssqldb"
1112
"github.com/microsoft/go-sqlcmd/internal/color"
1213
"github.com/stretchr/testify/assert"
1314
)
@@ -162,3 +163,42 @@ func TestFormatterXmlMode(t *testing.T) {
162163
assert.NoError(t, err, "runSqlCmd returned error")
163164
assert.Equal(t, `<sys.databases name="master"/>`+SqlcmdEol, buf.buf.String())
164165
}
166+
167+
func TestAddErrorStripsMssqlPrefixByDefault(t *testing.T) {
168+
out, errOut := new(strings.Builder), new(strings.Builder)
169+
vars := InitializeVariables(false)
170+
f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore)
171+
f.BeginBatch("", vars, out, errOut)
172+
173+
f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7})
174+
175+
got := errOut.String()
176+
assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7")
177+
assert.Contains(t, got, "Something failed")
178+
assert.NotContains(t, got, "mssql:")
179+
}
180+
181+
func TestAddErrorWithRawErrorsKeepsMssqlPrefix(t *testing.T) {
182+
out, errOut := new(strings.Builder), new(strings.Builder)
183+
vars := InitializeVariables(false)
184+
f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(true))
185+
f.BeginBatch("", vars, out, errOut)
186+
187+
f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7})
188+
189+
got := errOut.String()
190+
assert.Contains(t, got, "Msg 50000, Level 16, State 1, Server server, Line 7")
191+
assert.Contains(t, got, "mssql: Something failed")
192+
}
193+
194+
func TestAddErrorWithRawErrorsAppliesToAsciiFormatter(t *testing.T) {
195+
out, errOut := new(strings.Builder), new(strings.Builder)
196+
vars := InitializeVariables(false)
197+
vars.Set(SQLCMDFORMAT, "ascii")
198+
f := NewSQLCmdDefaultFormatter(vars, false, ControlIgnore, WithRawErrors(true))
199+
f.BeginBatch("", vars, out, errOut)
200+
201+
f.AddError(mssql.Error{Number: 50000, State: 1, Class: 16, Message: "Something failed", ServerName: "server", LineNo: 7})
202+
203+
assert.Contains(t, errOut.String(), "mssql: Something failed", "ascii formatter must honor WithRawErrors")
204+
}

0 commit comments

Comments
 (0)