diff --git a/client_base.go b/client_base.go index abb2f2b..c55acdd 100644 --- a/client_base.go +++ b/client_base.go @@ -275,6 +275,15 @@ func request( infolog.Println(err) return response{nil, 0, nil, ConstructNestedError("error during reading a request response", err)} } + // Error might be in the response body, despite the status code 200 + errorResponse := struct { + Errors []ErrorDetails `json:"errors"` + }{} + if err = json.Unmarshal(body, &errorResponse); err == nil { + if errorResponse.Errors != nil { + return response{nil, resp.StatusCode, nil, NewStructuredError(errorResponse.Errors)} + } + } if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { if err = checkErrorResponse(body); err != nil { diff --git a/driver_integration_test.go b/driver_integration_test.go index 97246c6..f14955c 100644 --- a/driver_integration_test.go +++ b/driver_integration_test.go @@ -301,3 +301,22 @@ func TestServiceAccountAuthentication(t *testing.T) { t.Errorf("Authentication didn't return an error with correct message, got: %s", err.Error()) } } + +func TestIncorrectQueryThrowingStructuredError(t *testing.T) { + db, err := sql.Open("firebolt", dsnSystemEngineMock) + if err != nil { + t.Errorf("failed unexpectedly with %v", err) + } + _, err = db.Query("SET advanced_mode=1") + raiseIfError(t, err) + _, err = db.Query("SET enable_json_error_output_format=true") + raiseIfError(t, err) + _, err = db.Query("SELECT 'blue'::int") + if err == nil { + t.Errorf("Query didn't return an error, although it should") + } + + if !strings.HasPrefix(err.Error(), "error during query execution: error during query request: Cannot parse string 'blue' as integer") { + t.Errorf("Query didn't return an error with correct message, got: %s", err.Error()) + } +} diff --git a/statement.go b/statement.go index bcd3ba2..4917326 100644 --- a/statement.go +++ b/statement.go @@ -10,11 +10,29 @@ type Column struct { Type string `json:"type"` } +type Location struct { + FailingLine int `json:"failingLine"` + StartOffset int `json:"startOffset"` + EndOffset int `json:"endOffset"` +} + +type ErrorDetails struct { + Code string `json:"code"` + Name string `json:"name"` + Severity string `json:"severity"` + Source string `json:"source"` + Description string `json:"description"` + Resolution string `json:"resolution"` + HelpLink string `json:"helpLink"` + Location Location `json:"location"` +} + type QueryResponse struct { Query interface{} `json:"query"` Meta []Column `json:"meta"` Data [][]interface{} `json:"data"` Rows int `json:"rows"` + Errors []ErrorDetails `json:"errors"` Statistics interface{} `json:"statistics"` } diff --git a/utils.go b/utils.go index a181d50..c253fe6 100644 --- a/utils.go +++ b/utils.go @@ -216,3 +216,64 @@ func valueToNamedValue(args []driver.Value) []driver.NamedValue { } return namedValues } + +type StructuredError struct { + Message string +} + +func (e StructuredError) Error() string { + return e.Message +} + +func NewStructuredError(errorDetails []ErrorDetails) *StructuredError { + // "{severity}: {name} ({code}) - {source}, {description}, resolution: {resolution} at {location} see {helpLink}" + message := strings.Builder{} + for _, error := range errorDetails { + if message.Len() > 0 { + message.WriteString("\n") + } + formatErrorDetails(&message, error) + } + return &StructuredError{ + Message: message.String(), + } +} +func formatErrorDetails(message *strings.Builder, error ErrorDetails) string { + if error.Severity != "" { + message.WriteString(fmt.Sprintf("%s: ", error.Severity)) + } + if error.Name != "" { + message.WriteString(fmt.Sprintf("%s ", error.Name)) + } + if error.Code != "" { + message.WriteString(fmt.Sprintf("(%s) ", error.Code)) + } + if error.Description != "" { + addDelimiterIfNotEmpty(message, "-") + message.WriteString(error.Description) + } + if error.Source != "" { + addDelimiterIfNotEmpty(message, ",") + message.WriteString(error.Source) + } + if error.Resolution != "" { + addDelimiterIfNotEmpty(message, ",") + message.WriteString(fmt.Sprintf("resolution: %s", error.Resolution)) + } + if error.Location.FailingLine != 0 || error.Location.StartOffset != 0 || error.Location.EndOffset != 0 { + addDelimiterIfNotEmpty(message, " at") + message.WriteString(fmt.Sprintf("%+v", error.Location)) + } + if error.HelpLink != "" { + addDelimiterIfNotEmpty(message, ",") + message.WriteString(fmt.Sprintf("see %s", error.HelpLink)) + } + return message.String() +} + +func addDelimiterIfNotEmpty(message *strings.Builder, delimiter string) { + if message.Len() > 0 { + message.WriteString(delimiter) + message.WriteString(" ") + } +} diff --git a/utils_test.go b/utils_test.go index 3c39849..bda4b93 100644 --- a/utils_test.go +++ b/utils_test.go @@ -196,3 +196,78 @@ func TestValueToNamedValue(t *testing.T) { assert(namedValues[0].Value, 2, t, "namedValues value is wrong") assert(namedValues[1].Value, "string", t, "namedValues value is wrong") } +func TestNewStructuredError(t *testing.T) { + errorDetails := ErrorDetails{ + Severity: "error", + Name: "TestError", + Code: "123", + Description: "This is a test error", + Source: "TestSource", + Resolution: "Please fix the error", + Location: Location{ + FailingLine: 10, + StartOffset: 20, + EndOffset: 30, + }, + HelpLink: "https://example.com", + } + + expectedMessage := "error: TestError (123) - This is a test error, TestSource, resolution: Please fix the error at {FailingLine:10 StartOffset:20 EndOffset:30}, see https://example.com" + + err := NewStructuredError([]ErrorDetails{errorDetails}) + + if err.Message != expectedMessage { + t.Errorf("NewStructuredError returned incorrect error message, got: %s, want: %s", err.Message, expectedMessage) + } +} + +func TestStructuredErrorWithMissingFields(t *testing.T) { + errorDetails := ErrorDetails{ + Severity: "error", + Name: "TestError", + Code: "123", + Description: "This is a test error", + } + + expectedMessage := "error: TestError (123) - This is a test error" + + err := NewStructuredError([]ErrorDetails{errorDetails}) + + if err.Message != expectedMessage { + t.Errorf("NewStructuredError returned incorrect error message, got: %s, want: %s", err.Message, expectedMessage) + } +} + +func TestStructuredErrorWithMultipleErrors(t *testing.T) { + errorDetails := ErrorDetails{ + Severity: "error", + Name: "TestError", + Code: "123", + Description: "This is a test error", + Source: "TestSource", + Resolution: "Please fix the error", + Location: Location{ + FailingLine: 10, + StartOffset: 20, + EndOffset: 30, + }, + HelpLink: "https://example.com", + } + + errorDetails2 := ErrorDetails{ + Severity: "error", + Name: "TestError", + Code: "123", + Description: "This is a test error", + Source: "TestSource", + Resolution: "Please fix the error", + } + + expectedMessage := "error: TestError (123) - This is a test error, TestSource, resolution: Please fix the error at {FailingLine:10 StartOffset:20 EndOffset:30}, see https://example.com\nerror: TestError (123) - This is a test error, TestSource, resolution: Please fix the error" + + err := NewStructuredError([]ErrorDetails{errorDetails, errorDetails2}) + + if err.Message != expectedMessage { + t.Errorf("NewStructuredError returned incorrect error message, got: %s, want: %s", err.Message, expectedMessage) + } +}