Skip to content

Commit

Permalink
bpo-16379: expose SQLite error codes and error names in sqlite3 (GH…
Browse files Browse the repository at this point in the history
  • Loading branch information
Erlend Egeberg Aasland authored Aug 30, 2021
1 parent f62763d commit 86d8b46
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 32 deletions.
5 changes: 4 additions & 1 deletion Doc/includes/sqlite3/complete_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
14 changes: 14 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=======
Expand Down
87 changes: 85 additions & 2 deletions Lib/sqlite3/test/dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add SQLite error code and name to :mod:`sqlite3` exceptions.
Patch by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland.
75 changes: 73 additions & 2 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions Modules/_sqlite/module.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
105 changes: 78 additions & 27 deletions Modules/_sqlite/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
}

Expand Down

0 comments on commit 86d8b46

Please sign in to comment.