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 53ea9439..00dae6b8 100644 --- a/errors.go +++ b/errors.go @@ -4,6 +4,7 @@ import "C" import ( "errors" "fmt" + "strings" ) func getError(errDriver error, err error) error { @@ -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: ") + if idx := strings.Index(errMsg, ": "); idx != -1 { + if typ, ok := errorPrefixMap[errMsg[:idx]]; ok { + errType = typ + } + } + return &DuckDBError{ + Type: errType, + Msg: errMsg, + } +} diff --git a/errors_test.go b/errors_test.go index 2e23933b..549131ea 100644 --- a/errors_test.go +++ b/errors_test.go @@ -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) + } +} 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 }