Skip to content

Commit

Permalink
Merge pull request #239 from apocelipes/feat-better-error
Browse files Browse the repository at this point in the history
support better error messages
  • Loading branch information
taniabogatsch authored Jul 1, 2024
2 parents b637da2 + c4a66b1 commit 2725c1c
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 9 deletions.
10 changes: 5 additions & 5 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ func (c *conn) prepareStmt(cmd string) (*stmt, error) {

var s C.duckdb_prepared_statement
if state := C.duckdb_prepare(c.duckdbCon, cmdstr, &s); state == C.DuckDBError {
dbErr := C.GoString(C.duckdb_prepare_error(s))
dbErr := getDuckDBError(C.GoString(C.duckdb_prepare_error(s)))
C.duckdb_destroy_prepare(&s)
return nil, errors.New(dbErr)
return nil, dbErr
}

return &stmt{c: c, stmt: &s}, nil
Expand All @@ -175,7 +175,7 @@ func (c *conn) extractStmts(query string) (C.duckdb_extracted_statements, C.idx_
err := C.GoString(C.duckdb_extract_statements_error(stmts))
C.duckdb_destroy_extracted(&stmts)
if err != "" {
return nil, 0, errors.New(err)
return nil, 0, getDuckDBError(err)
}
return nil, 0, errors.New("no statements found")
}
Expand All @@ -186,9 +186,9 @@ func (c *conn) extractStmts(query string) (C.duckdb_extracted_statements, C.idx_
func (c *conn) prepareExtractedStmt(extractedStmts C.duckdb_extracted_statements, index C.idx_t) (*stmt, error) {
var s C.duckdb_prepared_statement
if state := C.duckdb_prepare_extracted_statement(c.duckdbCon, extractedStmts, index, &s); state == C.DuckDBError {
dbErr := C.GoString(C.duckdb_prepare_error(s))
dbErr := getDuckDBError(C.GoString(C.duckdb_prepare_error(s)))
C.duckdb_destroy_prepare(&s)
return nil, errors.New(dbErr)
return nil, dbErr
}

return &stmt{c: c, stmt: &s}, nil
Expand Down
123 changes: 123 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "C"
import (
"errors"
"fmt"
"strings"
)

func getError(errDriver error, err error) error {
Expand Down Expand Up @@ -78,3 +79,125 @@ var (
errConnect = errors.New("could not connect to database")
errCreateConfig = errors.New("could not create config for database")
)

type DuckDBErrorType int

const (
ErrorTypeInvalid DuckDBErrorType = iota // invalid type
ErrorTypeOutOfRange // value out of range error
ErrorTypeConversion // conversion/casting error
ErrorTypeUnknownType // unknown type error
ErrorTypeDecimal // decimal related
ErrorTypeMismatchType // type mismatch
ErrorTypeDivideByZero // divide by 0
ErrorTypeObjectSize // object size exceeded
ErrorTypeInvalidType // incompatible for operation
ErrorTypeSerialization // serialization
ErrorTypeTransaction // transaction management
ErrorTypeNotImplemented // method not implemented
ErrorTypeExpression // expression parsing
ErrorTypeCatalog // catalog related
ErrorTypeParser // parser related
ErrorTypePlanner // planner related
ErrorTypeScheduler // scheduler related
ErrorTypeExecutor // executor related
ErrorTypeConstraint // constraint related
ErrorTypeIndex // index related
ErrorTypeStat // stat related
ErrorTypeConnection // connection related
ErrorTypeSyntax // syntax related
ErrorTypeSettings // settings related
ErrorTypeBinder // binder related
ErrorTypeNetwork // network related
ErrorTypeOptimizer // optimizer related
ErrorTypeNullPointer // nullptr exception
ErrorTypeIO // IO exception
ErrorTypeInterrupt // interrupt
ErrorTypeFatal // Fatal exceptions are non-recoverable, and render the entire DB in an unusable state
ErrorTypeInternal // Internal exceptions indicate something went wrong internally (i.e. bug in the code base)
ErrorTypeInvalidInput // Input or arguments error
ErrorTypeOutOfMemory // out of memory
ErrorTypePermission // insufficient permissions
ErrorTypeParameterNotResolved // parameter types could not be resolved
ErrorTypeParameterNotAllowed // parameter types not allowed
ErrorTypeDependency // dependency
ErrorTypeHTTP
ErrorTypeMissingExtension // Thrown when an extension is used but not loaded
ErrorTypeAutoLoad // Thrown when an extension is used but not loaded
ErrorTypeSequence
)

var errorPrefixMap = map[string]DuckDBErrorType{
"Invalid Error": ErrorTypeInvalid,
"Out of Range Error": ErrorTypeOutOfRange,
"Conversion Error": ErrorTypeConversion,
"Error": ErrorTypeUnknownType,
"Decimal Error": ErrorTypeDecimal,
"Mismatch Type Error": ErrorTypeMismatchType,
"Divide by Zero Error": ErrorTypeDivideByZero,
"Object Size Error": ErrorTypeObjectSize,
"Invalid type Error": ErrorTypeInvalidType,
"Serialization Error": ErrorTypeSerialization,
"TransactionContext Error": ErrorTypeTransaction,
"Not implemented Error": ErrorTypeNotImplemented,
"Expression Error": ErrorTypeExpression,
"Catalog Error": ErrorTypeCatalog,
"Parser Error": ErrorTypeParser,
"Planner Error": ErrorTypePlanner,
"Scheduler Error": ErrorTypeScheduler,
"Executor Error": ErrorTypeExecutor,
"Constraint Error": ErrorTypeConstraint,
"Index Error": ErrorTypeIndex,
"Stat Error": ErrorTypeStat,
"Connection Error": ErrorTypeConnection,
"Syntax Error": ErrorTypeSyntax,
"Settings Error": ErrorTypeSettings,
"Binder Error": ErrorTypeBinder,
"Network Error": ErrorTypeNetwork,
"Optimizer Error": ErrorTypeOptimizer,
"NullPointer Error": ErrorTypeNullPointer,
"IO Error": ErrorTypeIO,
"INTERRUPT Error": ErrorTypeInterrupt,
"FATAL Error": ErrorTypeFatal,
"INTERNAL Error": ErrorTypeInternal,
"Invalid Input Error": ErrorTypeInvalidInput,
"Out of Memory Error": ErrorTypeOutOfMemory,
"Permission Error": ErrorTypePermission,
"Parameter Not Resolved Error": ErrorTypeParameterNotResolved,
"Parameter Not Allowed Error": ErrorTypeParameterNotAllowed,
"Dependency Error": ErrorTypeDependency,
"HTTP Error": ErrorTypeHTTP,
"Missing Extension Error": ErrorTypeMissingExtension,
"Extension Autoloading Error": ErrorTypeAutoLoad,
"Sequence Error": ErrorTypeSequence,
}

type DuckDBError struct {
Type DuckDBErrorType
Msg string
}

func (de *DuckDBError) Error() string {
return de.Msg
}

func (de *DuckDBError) Is(err error) bool {
if derr, ok := err.(*DuckDBError); ok {
return derr.Msg == de.Msg
}
return false
}

func getDuckDBError(errMsg string) error {
errType := ErrorTypeInvalid
// find the end of the prefix ("<error-type> Error: ")
if idx := strings.Index(errMsg, ": "); idx != -1 {
if typ, ok := errorPrefixMap[errMsg[:idx]]; ok {
errType = typ
}
}
return &DuckDBError{
Type: errType,
Msg: errMsg,
}
}
86 changes: 86 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,89 @@ func TestErrAPISetValue(t *testing.T) {
err := chunk.SetValue(1, 42, "hello")
testError(t, err, errAPI.Error(), columnCountErrMsg)
}

func TestDuckDBErrors(t *testing.T) {
db := openDB(t)
defer db.Close()
createTable(db, t, `CREATE TABLE duckdberror_test(bar VARCHAR UNIQUE, baz INT32)`)
_, err := db.Exec("INSERT INTO duckdberror_test(bar, baz) VALUES('bar', 0)")
require.NoError(t, err)

testCases := []struct {
tpl string
errTyp DuckDBErrorType
}{
{
tpl: "SELECT * FROM not_exist WHERE baz=0",
errTyp: ErrorTypeCatalog,
},
{
tpl: "COPY duckdberror_test FROM 'test.json'",
errTyp: ErrorTypeCatalog,
},
{
tpl: "SELECT * FROM duckdberror_test WHERE col=?",
errTyp: ErrorTypeBinder,
},
{
tpl: "SELEC * FROM duckdberror_test baz=0",
errTyp: ErrorTypeParser,
},
{
tpl: "INSERT INTO duckdberror_test(bar, baz) VALUES('bar', 1)",
errTyp: ErrorTypeConstraint,
},
{
tpl: "INSERT INTO duckdberror_test(bar, baz) VALUES('foo', 18446744073709551615)",
errTyp: ErrorTypeConversion,
},
{
tpl: "INSTALL not_exist",
errTyp: ErrorTypeHTTP,
},
{
tpl: "LOAD not_exist",
errTyp: ErrorTypeIO,
},
}
for _, tc := range testCases {
_, err := db.Exec(tc.tpl)
de, ok := err.(*DuckDBError)
if !ok {
require.Fail(t, "error type is not DuckDBError", "tql: %s\ngot: %#v", tc.tpl, err)
}
require.Equal(t, de.Type, tc.errTyp, "tql: %s\nactual error msg: %s", tc.tpl, de.Msg)
}
}

func TestGetDuckDBError(t *testing.T) {
// only for the corner cases
testCases := []*DuckDBError{
{
Msg: "",
Type: ErrorTypeInvalid,
},
{
Msg: "Unknown",
Type: ErrorTypeInvalid,
},
{
Msg: "Error: xxx",
Type: ErrorTypeUnknownType,
},
// next two for the prefix testing
{
Msg: "Invalid Error: xxx",
Type: ErrorTypeInvalid,
},
{
Msg: "Invalid Input Error: xxx",
Type: ErrorTypeInvalidInput,
},
}

for _, tc := range testCases {
err := getDuckDBError(tc.Msg).(*DuckDBError)
require.Equal(t, tc, err)
}
}
8 changes: 4 additions & 4 deletions statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@ func (s *stmt) execute(ctx context.Context, args []driver.NamedValue) (*C.duckdb

var pendingRes C.duckdb_pending_result
if state := C.duckdb_pending_prepared(*s.stmt, &pendingRes); state == C.DuckDBError {
dbErr := C.GoString(C.duckdb_pending_error(pendingRes))
dbErr := getDuckDBError(C.GoString(C.duckdb_pending_error(pendingRes)))
C.duckdb_destroy_pending(&pendingRes)
return nil, errors.New(dbErr)
return nil, dbErr
}
defer C.duckdb_destroy_pending(&pendingRes)

Expand Down Expand Up @@ -254,9 +254,9 @@ func (s *stmt) execute(ctx context.Context, args []driver.NamedValue) (*C.duckdb
return nil, ctx.Err()
}

err := C.GoString(C.duckdb_result_error(&res))
err := getDuckDBError(C.GoString(C.duckdb_result_error(&res)))
C.duckdb_destroy_result(&res)
return nil, errors.New(err)
return nil, err
}

return &res, nil
Expand Down
3 changes: 3 additions & 0 deletions statement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func TestPrepareWithError(t *testing.T) {
for _, tc := range testCases {
stmt, err := db.Prepare(tc.tpl)
if err != nil {
if _, ok := err.(*DuckDBError); !ok {
require.Fail(t, "error type is not DuckDBError")
}
require.ErrorContains(t, err, tc.err)
continue
}
Expand Down

0 comments on commit 2725c1c

Please sign in to comment.