diff --git a/README.md b/README.md index fe26e192..825e2bd5 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ switches are most important to you to have implemented next in the new sqlcmd. - `:Connect` now has an optional `-G` parameter to select one of the authentication methods for Azure SQL Database - `SqlAuthentication`, `ActiveDirectoryDefault`, `ActiveDirectoryIntegrated`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, `ActiveDirectoryPassword`. If `-G` is not provided, either Integrated security or SQL Authentication will be used, dependent on the presence of a `-U` username parameter. - The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces. - Sqlcmd can now print results using a vertical format. Use the new `--vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable. +- The `-j` (or `--raw-errors`) flag prints raw error messages, without the `Msg`, `Level`, `State`, `Server`, and `Line` prefix that is normally prepended to SQL Server error messages. ``` 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 ea655b47..2f5a6634 100644 --- a/cmd/sqlcmd/sqlcmd.go +++ b/cmd/sqlcmd/sqlcmd.go @@ -82,6 +82,8 @@ type SQLCmdArguments struct { ChangePassword string ChangePasswordAndExit string TraceFile string + // RawErrors prints only the error message without Msg/Level/State header + RawErrors bool // Keep Help at the end of the list Help bool } @@ -452,6 +454,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 with no additional information")) //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")) @@ -828,7 +831,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(args.TrimSpaces, args.getControlCharacterBehavior(), 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 511816b2..e689a610 100644 --- a/cmd/sqlcmd/sqlcmd_test.go +++ b/cmd/sqlcmd/sqlcmd_test.go @@ -123,6 +123,16 @@ 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" }}, + // Test -j flag for raw error messages + {[]string{"-j"}, func(args SQLCmdArguments) bool { + return args.RawErrors + }}, + {[]string{"--raw-errors"}, func(args SQLCmdArguments) bool { + return args.RawErrors + }}, + {[]string{"-j", "-b"}, func(args SQLCmdArguments) bool { + return args.RawErrors && args.ExitOnError + }}, } for _, test := range commands { diff --git a/internal/sql/mssql.go b/internal/sql/mssql.go index 442e514a..d80e99e5 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(false, sqlcmd.ControlIgnore, false) connect := sqlcmd.ConnectSettings{ ServerName: fmt.Sprintf( "%s,%#v", diff --git a/pkg/sqlcmd/commands_test.go b/pkg/sqlcmd/commands_test.go index 6197aa3f..f7dc23ec 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(false, ControlIgnore, false) // 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..88531435 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -85,15 +85,17 @@ type sqlCmdFormatterType struct { maxColNameLen int colorizer color.Colorizer xml bool + rawErrors bool } // NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter -func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool, ccb ControlCharacterBehavior) Formatter { +func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool, ccb ControlCharacterBehavior, rawErrors bool) Formatter { return &sqlCmdFormatterType{ removeTrailingSpaces: removeTrailingSpaces, format: "horizontal", colorizer: color.New(false), ccb: ccb, + rawErrors: rawErrors, } } @@ -223,10 +225,13 @@ func (f *sqlCmdFormatterType) AddError(err error) { switch e := (err).(type) { case mssql.Error: if print = f.vars.ErrorLevel() <= 0 || e.Class >= uint8(f.vars.ErrorLevel()); print { - if len(e.ProcName) > 0 { - b.WriteString(localizer.Sprintf("Msg %#v, Level %d, State %d, Server %s, Procedure %s, Line %#v%s", e.Number, e.Class, e.State, e.ServerName, e.ProcName, e.LineNo, SqlcmdEol)) - } 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)) + // Only print the structured error header if rawErrors mode is not enabled + if !f.rawErrors { + if len(e.ProcName) > 0 { + b.WriteString(localizer.Sprintf("Msg %#v, Level %d, State %d, Server %s, Procedure %s, Line %#v%s", e.Number, e.Class, e.State, e.ServerName, e.ProcName, e.LineNo, SqlcmdEol)) + } 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: ") } diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 0f304632..96e33c78 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" ) @@ -158,3 +159,105 @@ func TestFormatterXmlMode(t *testing.T) { assert.NoError(t, err, "runSqlCmd returned error") assert.Equal(t, ``+SqlcmdEol, buf.buf.String()) } + +func TestFormatterRawErrors(t *testing.T) { + // Test that raw errors mode only prints the error message without the Msg/Level/State header + vars := InitializeVariables(false) + errBuf := new(strings.Builder) + + // Create formatter with rawErrors = false (default) + f := NewSQLCmdDefaultFormatter(false, ControlIgnore, false).(*sqlCmdFormatterType) + f.BeginBatch("", vars, new(strings.Builder), errBuf) + + // Create a mssql.Error to test with + testErr := mssql.Error{ + Number: 208, + Class: 16, + State: 1, + ServerName: "testserver", + Message: "Invalid object name 'nonexistent'.", + } + + f.AddError(testErr) + normalOutput := errBuf.String() + // Normal mode should include the Msg header + assert.Contains(t, normalOutput, "Msg 208") + assert.Contains(t, normalOutput, "Level 16") + assert.Contains(t, normalOutput, "State 1") + assert.Contains(t, normalOutput, "Invalid object name 'nonexistent'.") + + // Create formatter with rawErrors = true + errBuf.Reset() + f = NewSQLCmdDefaultFormatter(false, ControlIgnore, true).(*sqlCmdFormatterType) + f.BeginBatch("", vars, new(strings.Builder), errBuf) + + f.AddError(testErr) + rawOutput := errBuf.String() + // Raw mode should NOT include the Msg header + assert.NotContains(t, rawOutput, "Msg 208") + assert.NotContains(t, rawOutput, "Level 16") + assert.NotContains(t, rawOutput, "State 1") + // But should still contain the actual error message + assert.Contains(t, rawOutput, "Invalid object name 'nonexistent'.") +} + +func TestFormatterErrorWithProcName(t *testing.T) { + // Test that errors with ProcName include the Procedure in the header + vars := InitializeVariables(false) + errBuf := new(strings.Builder) + + // Create formatter with rawErrors = false (default) + f := NewSQLCmdDefaultFormatter(false, ControlIgnore, false).(*sqlCmdFormatterType) + f.BeginBatch("", vars, new(strings.Builder), errBuf) + + // Create a mssql.Error with ProcName to test with + testErr := mssql.Error{ + Number: 50000, + Class: 16, + State: 1, + ServerName: "testserver", + ProcName: "myStoredProc", + LineNo: 10, + Message: "Error raised from stored procedure.", + } + + f.AddError(testErr) + output := errBuf.String() + // Should include the Procedure in the header + assert.Contains(t, output, "Msg 50000") + assert.Contains(t, output, "Level 16") + assert.Contains(t, output, "State 1") + assert.Contains(t, output, "Server testserver") + assert.Contains(t, output, "Procedure myStoredProc") + assert.Contains(t, output, "Line 10") + assert.Contains(t, output, "Error raised from stored procedure.") +} + +func TestFormatterErrorWithProcNameRawMode(t *testing.T) { + // Test that errors with ProcName in raw mode skip header but still print message + vars := InitializeVariables(false) + errBuf := new(strings.Builder) + + // Create formatter with rawErrors = true + f := NewSQLCmdDefaultFormatter(false, ControlIgnore, true).(*sqlCmdFormatterType) + f.BeginBatch("", vars, new(strings.Builder), errBuf) + + testErr := mssql.Error{ + Number: 50000, + Class: 16, + State: 1, + ServerName: "testserver", + ProcName: "myStoredProc", + LineNo: 10, + Message: "Error raised from stored procedure.", + } + + f.AddError(testErr) + output := errBuf.String() + // Raw mode should NOT include the header + assert.NotContains(t, output, "Msg 50000") + assert.NotContains(t, output, "Level 16") + assert.NotContains(t, output, "Procedure myStoredProc") + // But should still contain the actual error message + assert.Contains(t, output, "Error raised from stored procedure.") +} diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index dfe97d1a..3fae3a33 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -619,7 +619,7 @@ 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(true, ControlIgnore, false) buf := &memoryBuffer{buf: new(bytes.Buffer)} s.SetOutput(buf) err := s.ConnectDb(nil, true) @@ -633,7 +633,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(true, ControlIgnore, false) file, err := os.CreateTemp("", "sqlcmdout") assert.NoError(t, err, "os.CreateTemp") s.SetOutput(file) @@ -651,7 +651,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(true, ControlIgnore, false) outfile, err := os.CreateTemp("", "sqlcmdout") assert.NoError(t, err, "os.CreateTemp") errfile, err := os.CreateTemp("", "sqlcmderr")