diff --git a/Doc/includes/sqlite3/complete_statement.py b/Doc/includes/sqlite3/complete_statement.py index cd38d7305bb69c..a5c947969910d4 100644 --- a/Doc/includes/sqlite3/complete_statement.py +++ b/Doc/includes/sqlite3/complete_statement.py @@ -24,7 +24,10 @@ if buffer.lstrip().upper().startswith("SELECT"): print(cur.fetchall()) except sqlite3.Error as e: - print("An error occurred:", e.args[0]) + err_msg = str(e) + err_code = e.sqlite_errorcode + err_name = e.sqlite_errorname + print(f"{err_name} ({err_code}): {err_msg}") buffer = "" con.close() diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 6399bed7ed52c6..7c60188bc70b59 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -836,6 +836,20 @@ Exceptions The base class of the other exceptions in this module. It is a subclass of :exc:`Exception`. + .. attribute:: sqlite_errorcode + + The numeric error code from the + `SQLite API `_ + + .. versionadded:: 3.11 + + .. attribute:: sqlite_errorname + + The symbolic name of the numeric error code + from the `SQLite API `_ + + .. versionadded:: 3.11 + .. exception:: DatabaseError Exception raised for errors that are related to the database. diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 306385c2a90aaf..1b736c71c24fbe 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -226,6 +226,12 @@ sqlite3 now raise :exc:`UnicodeEncodeError` instead of :exc:`sqlite3.ProgrammingError`. (Contributed by Erlend E. Aasland in :issue:`44688`.) +* :mod:`sqlite3` exceptions now include the SQLite error code as + :attr:`~sqlite3.Error.sqlite_errorcode` and the SQLite error name as + :attr:`~sqlite3.Error.sqlite_errorname`. + (Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in + :issue:`16379`.) + Removed ======= diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index bb9d5a7ce3e001..987a8f8b540ef7 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -28,12 +28,12 @@ import unittest from test.support import ( + SHORT_TIMEOUT, bigmemtest, check_disallow_instantiation, threading_helper, - SHORT_TIMEOUT, ) -from test.support.os_helper import TESTFN, unlink +from test.support.os_helper import TESTFN, unlink, temp_dir # Helper for tests using TESTFN @@ -102,6 +102,89 @@ def test_not_supported_error(self): sqlite.DatabaseError), "NotSupportedError is not a subclass of DatabaseError") + def test_module_constants(self): + consts = [ + "SQLITE_ABORT", + "SQLITE_ALTER_TABLE", + "SQLITE_ANALYZE", + "SQLITE_ATTACH", + "SQLITE_AUTH", + "SQLITE_BUSY", + "SQLITE_CANTOPEN", + "SQLITE_CONSTRAINT", + "SQLITE_CORRUPT", + "SQLITE_CREATE_INDEX", + "SQLITE_CREATE_TABLE", + "SQLITE_CREATE_TEMP_INDEX", + "SQLITE_CREATE_TEMP_TABLE", + "SQLITE_CREATE_TEMP_TRIGGER", + "SQLITE_CREATE_TEMP_VIEW", + "SQLITE_CREATE_TRIGGER", + "SQLITE_CREATE_VIEW", + "SQLITE_CREATE_VTABLE", + "SQLITE_DELETE", + "SQLITE_DENY", + "SQLITE_DETACH", + "SQLITE_DONE", + "SQLITE_DROP_INDEX", + "SQLITE_DROP_TABLE", + "SQLITE_DROP_TEMP_INDEX", + "SQLITE_DROP_TEMP_TABLE", + "SQLITE_DROP_TEMP_TRIGGER", + "SQLITE_DROP_TEMP_VIEW", + "SQLITE_DROP_TRIGGER", + "SQLITE_DROP_VIEW", + "SQLITE_DROP_VTABLE", + "SQLITE_EMPTY", + "SQLITE_ERROR", + "SQLITE_FORMAT", + "SQLITE_FULL", + "SQLITE_FUNCTION", + "SQLITE_IGNORE", + "SQLITE_INSERT", + "SQLITE_INTERNAL", + "SQLITE_INTERRUPT", + "SQLITE_IOERR", + "SQLITE_LOCKED", + "SQLITE_MISMATCH", + "SQLITE_MISUSE", + "SQLITE_NOLFS", + "SQLITE_NOMEM", + "SQLITE_NOTADB", + "SQLITE_NOTFOUND", + "SQLITE_OK", + "SQLITE_PERM", + "SQLITE_PRAGMA", + "SQLITE_PROTOCOL", + "SQLITE_READ", + "SQLITE_READONLY", + "SQLITE_REINDEX", + "SQLITE_ROW", + "SQLITE_SAVEPOINT", + "SQLITE_SCHEMA", + "SQLITE_SELECT", + "SQLITE_TOOBIG", + "SQLITE_TRANSACTION", + "SQLITE_UPDATE", + ] + if sqlite.version_info >= (3, 7, 17): + consts += ["SQLITE_NOTICE", "SQLITE_WARNING"] + if sqlite.version_info >= (3, 8, 3): + consts.append("SQLITE_RECURSIVE") + consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"] + for const in consts: + with self.subTest(const=const): + self.assertTrue(hasattr(sqlite, const)) + + def test_error_code_on_exception(self): + err_msg = "unable to open database file" + with temp_dir() as db: + with self.assertRaisesRegex(sqlite.Error, err_msg) as cm: + sqlite.connect(db) + e = cm.exception + self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN) + self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN") + # sqlite3_enable_shared_cache() is deprecated on macOS and calling it may raise # OperationalError on some buildbots. @unittest.skipIf(sys.platform == "darwin", "shared cache is deprecated on macOS") diff --git a/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst b/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst new file mode 100644 index 00000000000000..874a9cf77d8c01 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst @@ -0,0 +1,2 @@ +Add SQLite error code and name to :mod:`sqlite3` exceptions. +Patch by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland. diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 993e572c5cdb3e..47b1f7a9d0720c 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -282,12 +282,79 @@ static PyMethodDef module_methods[] = { {NULL, NULL} }; +/* SQLite API error codes */ +static const struct { + const char *name; + long value; +} error_codes[] = { +#define DECLARE_ERROR_CODE(code) {#code, code} + // Primary result code list + DECLARE_ERROR_CODE(SQLITE_ABORT), + DECLARE_ERROR_CODE(SQLITE_AUTH), + DECLARE_ERROR_CODE(SQLITE_BUSY), + DECLARE_ERROR_CODE(SQLITE_CANTOPEN), + DECLARE_ERROR_CODE(SQLITE_CONSTRAINT), + DECLARE_ERROR_CODE(SQLITE_CORRUPT), + DECLARE_ERROR_CODE(SQLITE_DONE), + DECLARE_ERROR_CODE(SQLITE_EMPTY), + DECLARE_ERROR_CODE(SQLITE_ERROR), + DECLARE_ERROR_CODE(SQLITE_FORMAT), + DECLARE_ERROR_CODE(SQLITE_FULL), + DECLARE_ERROR_CODE(SQLITE_INTERNAL), + DECLARE_ERROR_CODE(SQLITE_INTERRUPT), + DECLARE_ERROR_CODE(SQLITE_IOERR), + DECLARE_ERROR_CODE(SQLITE_LOCKED), + DECLARE_ERROR_CODE(SQLITE_MISMATCH), + DECLARE_ERROR_CODE(SQLITE_MISUSE), + DECLARE_ERROR_CODE(SQLITE_NOLFS), + DECLARE_ERROR_CODE(SQLITE_NOMEM), + DECLARE_ERROR_CODE(SQLITE_NOTADB), + DECLARE_ERROR_CODE(SQLITE_NOTFOUND), + DECLARE_ERROR_CODE(SQLITE_OK), + DECLARE_ERROR_CODE(SQLITE_PERM), + DECLARE_ERROR_CODE(SQLITE_PROTOCOL), + DECLARE_ERROR_CODE(SQLITE_READONLY), + DECLARE_ERROR_CODE(SQLITE_ROW), + DECLARE_ERROR_CODE(SQLITE_SCHEMA), + DECLARE_ERROR_CODE(SQLITE_TOOBIG), +#if SQLITE_VERSION_NUMBER >= 3007017 + DECLARE_ERROR_CODE(SQLITE_NOTICE), + DECLARE_ERROR_CODE(SQLITE_WARNING), +#endif +#undef DECLARE_ERROR_CODE + {NULL, 0}, +}; + +static int +add_error_constants(PyObject *module) +{ + for (int i = 0; error_codes[i].name != NULL; i++) { + const char *name = error_codes[i].name; + const long value = error_codes[i].value; + if (PyModule_AddIntConstant(module, name, value) < 0) { + return -1; + } + } + return 0; +} + +const char * +pysqlite_error_name(int rc) +{ + for (int i = 0; error_codes[i].name != NULL; i++) { + if (error_codes[i].value == rc) { + return error_codes[i].name; + } + } + // No error code matched. + return NULL; +} + static int add_integer_constants(PyObject *module) { int ret = 0; ret += PyModule_AddIntMacro(module, PARSE_DECLTYPES); ret += PyModule_AddIntMacro(module, PARSE_COLNAMES); - ret += PyModule_AddIntMacro(module, SQLITE_OK); ret += PyModule_AddIntMacro(module, SQLITE_DENY); ret += PyModule_AddIntMacro(module, SQLITE_IGNORE); ret += PyModule_AddIntMacro(module, SQLITE_CREATE_INDEX); @@ -325,7 +392,6 @@ static int add_integer_constants(PyObject *module) { #if SQLITE_VERSION_NUMBER >= 3008003 ret += PyModule_AddIntMacro(module, SQLITE_RECURSIVE); #endif - ret += PyModule_AddIntMacro(module, SQLITE_DONE); return ret; } @@ -406,6 +472,11 @@ PyMODINIT_FUNC PyInit__sqlite3(void) ADD_EXCEPTION(module, state, DataError, state->DatabaseError); ADD_EXCEPTION(module, state, NotSupportedError, state->DatabaseError); + /* Set error constants */ + if (add_error_constants(module) < 0) { + goto error; + } + /* Set integer constants */ if (add_integer_constants(module) < 0) { goto error; diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h index a286739579db6e..c273c1f9ed9f29 100644 --- a/Modules/_sqlite/module.h +++ b/Modules/_sqlite/module.h @@ -81,6 +81,8 @@ pysqlite_get_state_by_type(PyTypeObject *Py_UNUSED(tp)) return &pysqlite_global_state; } +extern const char *pysqlite_error_name(int rc); + #define PARSE_DECLTYPES 1 #define PARSE_COLNAMES 2 #endif diff --git a/Modules/_sqlite/util.c b/Modules/_sqlite/util.c index 24cefc626b66e2..cfd189dfc33608 100644 --- a/Modules/_sqlite/util.c +++ b/Modules/_sqlite/util.c @@ -36,27 +36,19 @@ pysqlite_step(sqlite3_stmt *statement) return rc; } -/** - * Checks the SQLite error code and sets the appropriate DB-API exception. - * Returns the error code (0 means no error occurred). - */ -int -_pysqlite_seterror(pysqlite_state *state, sqlite3 *db) +// Returns non-NULL if a new exception should be raised +static PyObject * +get_exception_class(pysqlite_state *state, int errorcode) { - int errorcode = sqlite3_errcode(db); - - switch (errorcode) - { + switch (errorcode) { case SQLITE_OK: PyErr_Clear(); - break; + return NULL; case SQLITE_INTERNAL: case SQLITE_NOTFOUND: - PyErr_SetString(state->InternalError, sqlite3_errmsg(db)); - break; + return state->InternalError; case SQLITE_NOMEM: - (void)PyErr_NoMemory(); - break; + return PyErr_NoMemory(); case SQLITE_ERROR: case SQLITE_PERM: case SQLITE_ABORT: @@ -70,26 +62,85 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db) case SQLITE_PROTOCOL: case SQLITE_EMPTY: case SQLITE_SCHEMA: - PyErr_SetString(state->OperationalError, sqlite3_errmsg(db)); - break; + return state->OperationalError; case SQLITE_CORRUPT: - PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db)); - break; + return state->DatabaseError; case SQLITE_TOOBIG: - PyErr_SetString(state->DataError, sqlite3_errmsg(db)); - break; + return state->DataError; case SQLITE_CONSTRAINT: case SQLITE_MISMATCH: - PyErr_SetString(state->IntegrityError, sqlite3_errmsg(db)); - break; + return state->IntegrityError; case SQLITE_MISUSE: - PyErr_SetString(state->ProgrammingError, sqlite3_errmsg(db)); - break; + return state->ProgrammingError; default: - PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db)); - break; + return state->DatabaseError; + } +} + +static void +raise_exception(PyObject *type, int errcode, const char *errmsg) +{ + PyObject *exc = NULL; + PyObject *args[] = { PyUnicode_FromString(errmsg), }; + if (args[0] == NULL) { + goto exit; + } + exc = PyObject_Vectorcall(type, args, 1, NULL); + Py_DECREF(args[0]); + if (exc == NULL) { + goto exit; + } + + PyObject *code = PyLong_FromLong(errcode); + if (code == NULL) { + goto exit; + } + int rc = PyObject_SetAttrString(exc, "sqlite_errorcode", code); + Py_DECREF(code); + if (rc < 0) { + goto exit; + } + + const char *error_name = pysqlite_error_name(errcode); + PyObject *name; + if (error_name) { + name = PyUnicode_FromString(error_name); + } + else { + name = PyUnicode_InternFromString("unknown"); + } + if (name == NULL) { + goto exit; + } + rc = PyObject_SetAttrString(exc, "sqlite_errorname", name); + Py_DECREF(name); + if (rc < 0) { + goto exit; + } + + PyErr_SetObject(type, exc); + +exit: + Py_XDECREF(exc); +} + +/** + * Checks the SQLite error code and sets the appropriate DB-API exception. + * Returns the error code (0 means no error occurred). + */ +int +_pysqlite_seterror(pysqlite_state *state, sqlite3 *db) +{ + int errorcode = sqlite3_errcode(db); + PyObject *exc_class = get_exception_class(state, errorcode); + if (exc_class == NULL) { + // No new exception need be raised; just pass the error code + return errorcode; } + /* Create and set the exception. */ + const char *errmsg = sqlite3_errmsg(db); + raise_exception(exc_class, errorcode, errmsg); return errorcode; }