From 83ab267abe9985664a5989488bd93e09aeefc963 Mon Sep 17 00:00:00 2001 From: palaviv Date: Thu, 13 Apr 2017 12:39:08 +0300 Subject: [PATCH 1/5] sqlite3 module expose sqlite error code and name in exceptions --- Doc/includes/sqlite3/complete_statement.py | 5 +- Doc/library/sqlite3.rst | 20 +++++ Lib/sqlite3/test/dbapi.py | 8 ++ Modules/_sqlite/module.c | 96 ++++++++++++++++++++-- Modules/_sqlite/module.h | 2 + Modules/_sqlite/util.c | 64 +++++++++++++-- 6 files changed, 179 insertions(+), 16 deletions(-) diff --git a/Doc/includes/sqlite3/complete_statement.py b/Doc/includes/sqlite3/complete_statement.py index cd38d7305bb69c..7094264adb7ecc 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]) + msg = str(e)) + error_code = e.sqlite_errorcode + error_name = e.sqlite_name + print(f"Error {error_name} [Errno {error_code}]: {msg}") buffer = "" con.close() diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 37087ac5af492a..97f1c0be1c3979 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -270,6 +270,26 @@ Module functions and constants disable the feature again. +.. exception:: Error + + Raised to signal an error from the underlying SQLite library. + + .. attribute:: sqlite_errorcode + + The numeric error code from the `SQLite API + `_. + + .. versionadded:: 3.7 + + .. attribute:: sqlite_errorname + + The symbolic name of the numeric error code + from the `SQLite API + `_. + + .. versionadded:: 3.7 + + .. _sqlite3-connection-objects: Connection Objects diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index 7c259d2af418fe..ba8c8939dc6148 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -83,6 +83,14 @@ def CheckNotSupportedError(self): sqlite.DatabaseError), "NotSupportedError is not a subclass of DatabaseError") + def CheckErrorCodeOnException(self): + with self.assertRaises(sqlite.Error) as cm: + db = sqlite.connect('/no/such/file/exists') + e = cm.exception + self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN) + self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN") + self.assertEqual(str(e), "unable to open database file") + class ConnectionTests(unittest.TestCase): def setUp(self): diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 274ee13c375eeb..96a5896fea3130 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -271,13 +271,70 @@ struct _IntConstantPair { typedef struct _IntConstantPair IntConstantPair; +/* sqlite API error codes */ +static const IntConstantPair _error_codes[] = { + {"SQLITE_OK", SQLITE_OK}, + {"SQLITE_ERROR", SQLITE_ERROR}, + {"SQLITE_INTERNAL", SQLITE_INTERNAL}, + {"SQLITE_PERM", SQLITE_PERM}, + {"SQLITE_ABORT", SQLITE_ABORT}, + {"SQLITE_BUSY", SQLITE_BUSY}, + {"SQLITE_LOCKED", SQLITE_LOCKED}, + {"SQLITE_NOMEM", SQLITE_NOMEM}, + {"SQLITE_READONLY", SQLITE_READONLY}, + {"SQLITE_INTERRUPT", SQLITE_INTERRUPT}, + {"SQLITE_IOERR", SQLITE_IOERR}, + {"SQLITE_CORRUPT", SQLITE_CORRUPT}, + {"SQLITE_NOTFOUND", SQLITE_NOTFOUND}, + {"SQLITE_FULL", SQLITE_FULL}, + {"SQLITE_CANTOPEN", SQLITE_CANTOPEN}, + {"SQLITE_PROTOCOL", SQLITE_PROTOCOL}, + {"SQLITE_EMPTY", SQLITE_EMPTY}, + {"SQLITE_SCHEMA", SQLITE_SCHEMA}, + {"SQLITE_TOOBIG", SQLITE_TOOBIG}, + {"SQLITE_CONSTRAINT", SQLITE_CONSTRAINT}, + {"SQLITE_MISMATCH", SQLITE_MISMATCH}, + {"SQLITE_MISUSE", SQLITE_MISUSE}, +#ifdef SQLITE_NOLFS + {"SQLITE_NOLFS", SQLITE_NOLFS}, +#endif +#ifdef SQLITE_AUTH + {"SQLITE_AUTH", SQLITE_AUTH}, +#endif +#ifdef SQLITE_FORMAT + {"SQLITE_FORMAT", SQLITE_FORMAT}, +#endif +#ifdef SQLITE_RANGE + {"SQLITE_RANGE", SQLITE_RANGE}, +#endif +#ifdef SQLITE_NOTADB + {"SQLITE_NOTADB", SQLITE_NOTADB}, +#endif + {"SQLITE_DONE", SQLITE_DONE}, + {"SQLITE_ROW", SQLITE_ROW}, + {(char*)NULL, 0}, + {"SQLITE_UNKNOWN", -1} +}; + +const char *sqlite3ErrName(int rc) { + int i; + for (i = 0; _error_codes[i].constant_name != 0; i++) { + if (_error_codes[i].constant_value == rc) + return _error_codes[i].constant_name; + } + // No error code matched. + return _error_codes[i+1].constant_name; +} + static const IntConstantPair _int_constants[] = { {"PARSE_DECLTYPES", PARSE_DECLTYPES}, {"PARSE_COLNAMES", PARSE_COLNAMES}, - {"SQLITE_OK", SQLITE_OK}, + /* enumerated return values for sqlite3_set_authorizer() callback */ {"SQLITE_DENY", SQLITE_DENY}, {"SQLITE_IGNORE", SQLITE_IGNORE}, + + /* enumerated values for sqlite3_set_authorizer() callback */ {"SQLITE_CREATE_INDEX", SQLITE_CREATE_INDEX}, {"SQLITE_CREATE_TABLE", SQLITE_CREATE_TABLE}, {"SQLITE_CREATE_TEMP_INDEX", SQLITE_CREATE_TEMP_INDEX}, @@ -342,6 +399,29 @@ static struct PyModuleDef _sqlite3module = { NULL }; + +static int add_to_dict(PyObject *dict, const char *key, int value) +{ + int sawerror; + PyObject *value_obj = PyLong_FromLong(value); + PyObject *name = PyUnicode_FromString(key); + + if (!value_obj || !name) { + Py_XDECREF(name); + Py_XDECREF(value_obj); + return 1; + } + + sawerror = PyDict_SetItem(dict, name, value_obj) < 0; + + Py_DECREF(value_obj); + Py_DECREF(name); + + if (sawerror) + return 1; + return 0; +} + PyMODINIT_FUNC PyInit__sqlite3(void) { PyObject *module, *dict; @@ -445,12 +525,16 @@ PyMODINIT_FUNC PyInit__sqlite3(void) /* Set integer constants */ for (i = 0; _int_constants[i].constant_name != NULL; i++) { - tmp_obj = PyLong_FromLong(_int_constants[i].constant_value); - if (!tmp_obj) { + if (add_to_dict(dict, _int_constants[i].constant_name, + _int_constants[i].constant_value) != 0) + goto error; + } + + /* Set error constants */ + for (i = 0; _error_codes[i].constant_name != 0; i++) { + if (add_to_dict(dict, _error_codes[i].constant_name, + _error_codes[i].constant_value) != 0) goto error; - } - PyDict_SetItemString(dict, _int_constants[i].constant_name, tmp_obj); - Py_DECREF(tmp_obj); } if (!(tmp_obj = PyUnicode_FromString(PYSQLITE_VERSION))) { diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h index 3185ec97888567..8db22339e7437f 100644 --- a/Modules/_sqlite/module.h +++ b/Modules/_sqlite/module.h @@ -48,6 +48,8 @@ extern PyObject* _pysqlite_converters; extern int _pysqlite_enable_callback_tracebacks; extern int pysqlite_BaseTypeAdapted; +extern const char *sqlite3ErrName(int rc); + #define PARSE_DECLTYPES 1 #define PARSE_COLNAMES 2 #endif diff --git a/Modules/_sqlite/util.c b/Modules/_sqlite/util.c index 3fa671d052b0d8..ecee9e16d95266 100644 --- a/Modules/_sqlite/util.c +++ b/Modules/_sqlite/util.c @@ -47,20 +47,21 @@ int pysqlite_step(sqlite3_stmt* statement, pysqlite_Connection* connection) */ int _pysqlite_seterror(sqlite3* db, sqlite3_stmt* st) { + PyObject *exc_class; int errorcode = sqlite3_errcode(db); switch (errorcode) { case SQLITE_OK: PyErr_Clear(); - break; + return errorcode; case SQLITE_INTERNAL: case SQLITE_NOTFOUND: - PyErr_SetString(pysqlite_InternalError, sqlite3_errmsg(db)); + exc_class = pysqlite_InternalError; break; case SQLITE_NOMEM: (void)PyErr_NoMemory(); - break; + return errorcode; case SQLITE_ERROR: case SQLITE_PERM: case SQLITE_ABORT: @@ -74,26 +75,71 @@ int _pysqlite_seterror(sqlite3* db, sqlite3_stmt* st) case SQLITE_PROTOCOL: case SQLITE_EMPTY: case SQLITE_SCHEMA: - PyErr_SetString(pysqlite_OperationalError, sqlite3_errmsg(db)); + exc_class = pysqlite_OperationalError; break; case SQLITE_CORRUPT: - PyErr_SetString(pysqlite_DatabaseError, sqlite3_errmsg(db)); + exc_class = pysqlite_DatabaseError; break; case SQLITE_TOOBIG: - PyErr_SetString(pysqlite_DataError, sqlite3_errmsg(db)); + exc_class = pysqlite_DataError; break; case SQLITE_CONSTRAINT: case SQLITE_MISMATCH: - PyErr_SetString(pysqlite_IntegrityError, sqlite3_errmsg(db)); + exc_class = pysqlite_IntegrityError; break; case SQLITE_MISUSE: - PyErr_SetString(pysqlite_ProgrammingError, sqlite3_errmsg(db)); + exc_class = pysqlite_ProgrammingError; break; default: - PyErr_SetString(pysqlite_DatabaseError, sqlite3_errmsg(db)); + exc_class = pysqlite_DatabaseError; break; } + /* Create and set the exception. */ + { + const char *error_msg; + const char *error_name; + PyObject *exc = NULL; + PyObject *args = NULL; + PyObject *py_code = NULL; + PyObject *py_name = NULL; + + error_name = sqlite3ErrName(errorcode); + + error_msg = sqlite3_errmsg(db); + + args = Py_BuildValue("(s)", error_msg); + if (!args) + goto error; + + exc = PyObject_Call(exc_class, args, NULL); + if (!exc) + goto error; + + py_code = Py_BuildValue("i", errorcode); + if (!py_code) + goto error; + + if (PyObject_SetAttrString(exc, "sqlite_errorcode", py_code) < 0) + goto error; + + py_name = Py_BuildValue("s", error_name); + if (!py_name) + goto error; + + if (PyObject_SetAttrString(exc, "sqlite_errorname", py_name) < 0) + goto error; + + PyErr_SetObject((PyObject *) Py_TYPE(exc), exc); + + error: + Py_XDECREF(py_code); + Py_XDECREF(py_name); + Py_XDECREF(args); + Py_XDECREF(exc); + } + + return errorcode; } From eeb10f4b17a5c9c3b45fd23aae8c7a706478d715 Mon Sep 17 00:00:00 2001 From: palaviv Date: Thu, 13 Apr 2017 19:09:06 +0300 Subject: [PATCH 2/5] Fix syntax error --- Doc/includes/sqlite3/complete_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/includes/sqlite3/complete_statement.py b/Doc/includes/sqlite3/complete_statement.py index 7094264adb7ecc..8b52087368f503 100644 --- a/Doc/includes/sqlite3/complete_statement.py +++ b/Doc/includes/sqlite3/complete_statement.py @@ -24,7 +24,7 @@ if buffer.lstrip().upper().startswith("SELECT"): print(cur.fetchall()) except sqlite3.Error as e: - msg = str(e)) + msg = str(e) error_code = e.sqlite_errorcode error_name = e.sqlite_name print(f"Error {error_name} [Errno {error_code}]: {msg}") From 42636f9a96d451223cb73f3d51d63e7d7bb7e295 Mon Sep 17 00:00:00 2001 From: Aviv Palivoda Date: Wed, 8 May 2019 15:15:14 +0300 Subject: [PATCH 3/5] Add NEWS entry --- .../next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst 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..2b5d657bebc011 --- /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 the exceptions of the sqlite3 module. +Patch by Aviv Palivoda based on work by Daniel Shahaf. From f2242237c75dc45c32af68585270a821a019d2ed Mon Sep 17 00:00:00 2001 From: Aviv Palivoda Date: Wed, 8 May 2019 15:22:34 +0300 Subject: [PATCH 4/5] Update version --- Doc/library/sqlite3.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 97f1c0be1c3979..c1b1617d9c0461 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -279,7 +279,7 @@ Module functions and constants The numeric error code from the `SQLite API `_. - .. versionadded:: 3.7 + .. versionadded:: 3.8 .. attribute:: sqlite_errorname @@ -287,7 +287,7 @@ Module functions and constants from the `SQLite API `_. - .. versionadded:: 3.7 + .. versionadded:: 3.8 .. _sqlite3-connection-objects: From 31467e3db0f7c47a9437843a0ead852b3c3a2ca7 Mon Sep 17 00:00:00 2001 From: Aviv Palivoda Date: Wed, 8 May 2019 15:36:55 +0300 Subject: [PATCH 5/5] Fix whitespace --- Lib/sqlite3/test/dbapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index ba8c8939dc6148..001e8383be648b 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -85,7 +85,7 @@ def CheckNotSupportedError(self): def CheckErrorCodeOnException(self): with self.assertRaises(sqlite.Error) as cm: - db = sqlite.connect('/no/such/file/exists') + db = sqlite.connect('/no/such/file/exists') e = cm.exception self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN) self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN")