From f74d6fac49dac1ea5cea2d2796a174467cde8521 Mon Sep 17 00:00:00 2001 From: apocelipes Date: Sun, 23 Jun 2024 15:47:35 +0800 Subject: [PATCH] support better error messages For #238. --- connection.go | 10 ++-- errors.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++ errors_test.go | 38 ++++++++++++++ statement.go | 8 +-- statement_test.go | 3 ++ 5 files changed, 174 insertions(+), 9 deletions(-) diff --git a/connection.go b/connection.go index 63ae13d7..a5e250f1 100644 --- a/connection.go +++ b/connection.go @@ -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 @@ -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") } @@ -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 diff --git a/errors.go b/errors.go index 978b555b..ce46874b 100644 --- a/errors.go +++ b/errors.go @@ -4,6 +4,7 @@ import "C" import ( "errors" "fmt" + "strings" ) func getError(errDriver error, err error) error { @@ -80,3 +81,126 @@ 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 + 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 + DuckDBExceptionUnknown DuckDBErrorType = -1 +) + +var exceptionPrefixMap = map[DuckDBErrorType]string{ + ErrorTypeInvalid: "Invalid", + ErrorTypeOutOfRange: "Out of Range", + ErrorTypeConversion: "Conversion", + ErrorTypeUnknownType: "Unknown Type", + ErrorTypeDecimal: "Decimal", + ErrorTypeMismatchType: "Mismatch Type", + ErrorTypeDivideByZero: "Divide by Zero", + ErrorTypeObjectSize: "Object Size", + ErrorTypeInvalidType: "Invalid type", + ErrorTypeSerialization: "Serialization", + ErrorTypeTransaction: "TransactionContext", + ErrorTypeNotImplemented: "Not implemented", + ErrorTypeExpression: "Expression", + ErrorTypeCatalog: "Catalog", + ErrorTypeParser: "Parser", + ErrorTypePlanner: "Planner", + ErrorTypeScheduler: "Scheduler", + ErrorTypeExecutor: "Executor", + ErrorTypeConstraint: "Constraint", + ErrorTypeIndex: "Index", + ErrorTypeStat: "Stat", + ErrorTypeConnection: "Connection", + ErrorTypeSyntax: "Syntax", + ErrorTypeSettings: "Settings", + ErrorTypeBinder: "Binder", + ErrorTypeNetwork: "Network", + ErrorTypeOptimizer: "Optimizer", + ErrorTypeNullPointer: "NullPointer", + ErrorTypeIO: "IO", + ErrorTypeInterrupt: "INTERRUPT", + ErrorTypeFatal: "FATAL", + ErrorTypeInternal: "INTERNAL", + ErrorTypeInvalidInput: "Invalid Input", + ErrorTypeOutOfMemory: "Out of Memory", + ErrorTypePermission: "Permission", + ErrorTypeParameterNotResolved: "Parameter Not Resolved", + ErrorTypeParameterNotAllowed: "Parameter Not Allowed", + ErrorTypeDependency: "Dependency", + ErrorTypeHTTP: "HTTP", + ErrorTypeMissingExtension: "Missing Extension", + ErrorTypeAutoLoad: "Extension Autoloading", + ErrorTypeSequence: "Sequence", +} + +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 := DuckDBExceptionUnknown + for k, v := range exceptionPrefixMap { + if strings.HasPrefix(errMsg, v+" Error") { + errType = k + break + } + } + return &DuckDBError{ + Type: errType, + Msg: errMsg, + } +} diff --git a/errors_test.go b/errors_test.go index bf210982..804f05db 100644 --- a/errors_test.go +++ b/errors_test.go @@ -273,3 +273,41 @@ func TestErrAPISetValue(t *testing.T) { err := chunk.SetValue(1, 42, "hello") testError(t, err, errAPI.Error(), columnCountErrMsg) } + +func TestGetDuckDBError(t *testing.T) { + testCases := []struct { + msg string + typ DuckDBErrorType + }{ + { + msg: "", + typ: DuckDBExceptionUnknown, + }, + { + msg: "Unknown", + typ: DuckDBExceptionUnknown, + }, + { + msg: "Unknown Type Error: xxx", + typ: ErrorTypeUnknownType, + }, + { + msg: "Constraint Error: Duplicate key \"key\" violates unique constraint. If this is an unexpected constraint violation please double check with the known index limitations section in our documentation (https://duckdb.org/docs/sql/indexes).", + typ: ErrorTypeConstraint, + }, + { + msg: "Invalid Error: xxx", + typ: ErrorTypeInvalid, + }, + { + msg: "Invalid Input Error: xxx", + typ: ErrorTypeInvalidInput, + }, + } + + for _, tc := range testCases { + err := getDuckDBError(tc.msg).(*DuckDBError) + require.Equal(t, tc.typ, err.Type) + require.Equal(t, tc.msg, err.Msg) + } +} diff --git a/statement.go b/statement.go index c3932205..0de5636a 100644 --- a/statement.go +++ b/statement.go @@ -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) @@ -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 diff --git a/statement_test.go b/statement_test.go index 43f423ee..b1ba9f31 100644 --- a/statement_test.go +++ b/statement_test.go @@ -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 }