[Python-checkins] bpo-16379: expose SQLite error codes and error names in `sqlite3` (GH-27786)

pablogsal webhook-mailer at python.org
Mon Aug 30 14:32:29 EDT 2021


https://github.com/python/cpython/commit/86d8b465231473f850cc5e906013ba8581ddb503
commit: 86d8b465231473f850cc5e906013ba8581ddb503
branch: main
author: Erlend Egeberg Aasland <erlend.aasland at innova.no>
committer: pablogsal <Pablogsal at gmail.com>
date: 2021-08-30T19:32:21+01:00
summary:

bpo-16379: expose SQLite error codes and error names in `sqlite3` (GH-27786)

files:
A Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst
M Doc/includes/sqlite3/complete_statement.py
M Doc/library/sqlite3.rst
M Doc/whatsnew/3.11.rst
M Lib/sqlite3/test/dbapi.py
M Modules/_sqlite/module.c
M Modules/_sqlite/module.h
M Modules/_sqlite/util.c

diff --git a/Doc/includes/sqlite3/complete_statement.py b/Doc/includes/sqlite3/complete_statement.py
index cd38d7305bb69..a5c947969910d 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 6399bed7ed52c..7c60188bc70b5 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 <https://sqlite.org/rescode.html>`_
+
+      .. versionadded:: 3.11
+
+   .. attribute:: sqlite_errorname
+
+      The symbolic name of the numeric error code
+      from the `SQLite API <https://sqlite.org/rescode.html>`_
+
+      .. 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 306385c2a90aa..1b736c71c24fb 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 aadecad32adb2..02e42e8c3751f 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 0000000000000..874a9cf77d8c0
--- /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 993e572c5cdb3..47b1f7a9d0720 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 a286739579db6..c273c1f9ed9f2 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 24cefc626b66e..cfd189dfc3360 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;
 }
 



More information about the Python-checkins mailing list