diff --git a/error.go b/error.go index 0c4992f..696281c 100644 --- a/error.go +++ b/error.go @@ -5,7 +5,15 @@ package sqlite3 +/* +#ifndef USE_LIBSQLITE3 +#include +#else +#include +#endif +*/ import "C" +import "syscall" // ErrNo inherit errno. type ErrNo int @@ -20,6 +28,7 @@ type ErrNoExtended int type Error struct { Code ErrNo /* The error code returned by SQLite */ ExtendedCode ErrNoExtended /* The extended error code returned by SQLite */ + SystemErrno syscall.Errno /* The system errno returned by the OS through SQLite, if applicable */ err string /* The error string returned by sqlite3_errmsg(), this usually contains more specific details. */ } @@ -72,10 +81,16 @@ func (err ErrNoExtended) Error() string { } func (err Error) Error() string { + var str string if err.err != "" { - return err.err + str = err.err + } else { + str = C.GoString(C.sqlite3_errstr(C.int(err.Code))) } - return errorString(err) + if err.SystemErrno != 0 { + str += ": " + err.SystemErrno.Error() + } + return str } // result codes from http://www.sqlite.org/c3ref/c_abort_rollback.html diff --git a/error_test.go b/error_test.go index 7cb33ae..3cfad06 100644 --- a/error_test.go +++ b/error_test.go @@ -240,5 +240,36 @@ func TestExtendedErrorCodes_Unique(t *testing.T) { extended, expected) } } - +} + +func TestError_SystemErrno(t *testing.T) { + _, n, _ := Version() + if n < 3012000 { + t.Skip("sqlite3_system_errno requires sqlite3 >= 3.12.0") + } + + // open a non-existent database in read-only mode so we get an IO error. + db, err := sql.Open("sqlite3", "file:nonexistent.db?mode=ro") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + err = db.Ping() + if err == nil { + t.Fatal("expected error pinging read-only non-existent database, but got nil") + } + + serr, ok := err.(Error) + if !ok { + t.Fatalf("expected error to be of type Error, but got %[1]T %[1]v", err) + } + + if serr.SystemErrno == 0 { + t.Fatal("expected SystemErrno to be set") + } + + if !os.IsNotExist(serr.SystemErrno) { + t.Errorf("expected SystemErrno to be a not exists error, but got %v", serr.SystemErrno) + } } diff --git a/sqlite3.go b/sqlite3.go index 7f0e7c0..61eeff7 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -183,6 +183,12 @@ static int _sqlite3_limit(sqlite3* db, int limitId, int newLimit) { return sqlite3_limit(db, limitId, newLimit); #endif } + +#if SQLITE_VERSION_NUMBER < 3012000 +static int sqlite3_system_errno(sqlite3 *db) { + return 0; +} +#endif */ import "C" import ( @@ -198,6 +204,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "unsafe" ) @@ -749,15 +756,28 @@ func (c *SQLiteConn) lastError() error { return lastError(c.db) } +// Note: may be called with db == nil func lastError(db *C.sqlite3) error { - rv := C.sqlite3_errcode(db) + rv := C.sqlite3_errcode(db) // returns SQLITE_NOMEM if db == nil if rv == C.SQLITE_OK { return nil } + extrv := C.sqlite3_extended_errcode(db) // returns SQLITE_NOMEM if db == nil + errStr := C.GoString(C.sqlite3_errmsg(db)) // returns "out of memory" if db == nil + + // https://www.sqlite.org/c3ref/system_errno.html + // sqlite3_system_errno is only meaningful if the error code was SQLITE_CANTOPEN, + // or it was SQLITE_IOERR and the extended code was not SQLITE_IOERR_NOMEM + var systemErrno syscall.Errno + if rv == C.SQLITE_CANTOPEN || (rv == C.SQLITE_IOERR && extrv != C.SQLITE_IOERR_NOMEM) { + systemErrno = syscall.Errno(C.sqlite3_system_errno(db)) + } + return Error{ Code: ErrNo(rv), - ExtendedCode: ErrNoExtended(C.sqlite3_extended_errcode(db)), - err: C.GoString(C.sqlite3_errmsg(db)), + ExtendedCode: ErrNoExtended(extrv), + SystemErrno: systemErrno, + err: errStr, } } @@ -869,10 +889,6 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) { return &SQLiteTx{c}, nil } -func errorString(err Error) string { - return C.GoString(C.sqlite3_errstr(C.int(err.Code))) -} - // Open database and return a new connection. // // A pragma can take either zero or one argument. @@ -1342,10 +1358,13 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE, nil) if rv != 0 { + // Save off the error _before_ closing the database. + // This is safe even if db is nil. + err := lastError(db) if db != nil { C.sqlite3_close_v2(db) } - return nil, Error{Code: ErrNo(rv)} + return nil, err } if db == nil { return nil, errors.New("sqlite succeeded without returning a database") diff --git a/sqlite3_test.go b/sqlite3_test.go index c8ef6d4..4b8fe01 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -305,8 +305,8 @@ func TestInsert(t *testing.T) { func TestUpsert(t *testing.T) { _, n, _ := Version() - if !(n >= 3024000) { - t.Skip("UPSERT requires sqlite3 => 3.24.0") + if n < 3024000 { + t.Skip("UPSERT requires sqlite3 >= 3.24.0") } tempFilename := TempFilename(t) defer os.Remove(tempFilename)