Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-16379: expose SQLite error codes and error names in sqlite3 #27786

Merged
merged 34 commits into from Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
83ab267
sqlite3 module expose sqlite error code and name in exceptions
palaviv Apr 13, 2017
eeb10f4
Fix syntax error
palaviv Apr 13, 2017
42636f9
Add NEWS entry
palaviv May 8, 2019
f224223
Update version
palaviv May 8, 2019
31467e3
Fix whitespace
palaviv May 8, 2019
679280d
Merge branch 'main' into sqlite-expose-error-code
Aug 6, 2021
8aa0859
Move exception documentation to the exceptions section
Aug 6, 2021
e4195c0
Use PyModule_AddObjectRef to add error codes
Aug 6, 2021
a4a0762
Refactor: add add_error_constants()
Aug 6, 2021
c18d227
Sort error codes alphabetically
Aug 6, 2021
67e49e8
Normalise style with the rest of the code base (PEP 7, etc.)
Aug 6, 2021
1893ced
Improve "get error name" function
Aug 6, 2021
67ea5f2
Refactor _pysqlite_seterror changes
Aug 6, 2021
6ced38c
Refactor error code table
Aug 6, 2021
467f7ba
Use macro to create error table
Aug 6, 2021
4abd093
Add missing SQLITE_NOTICE and SQLITE_WARNING codes
Aug 6, 2021
70507dd
Add comment
Aug 6, 2021
b617192
Merge branch 'main' into sqlite-expose-error-code
Aug 9, 2021
db053b9
Assert exception class is always set
Aug 9, 2021
98106dd
Use explicit API's iso. Py_BuildValue, etc.
Aug 9, 2021
0567938
Remove duplicate return code entries
Aug 9, 2021
16889a1
Group stuff
Aug 9, 2021
a7a1cf8
Use 'unknown' when a proper SQLite exception cannot be found
Aug 9, 2021
772a29c
Fix complete_statement.py example
Aug 9, 2021
fa5a1af
Improve unit tests
Aug 9, 2021
cf551b1
Adjust NEWS entry and update What's New
Aug 9, 2021
a3646c9
Use PyObject_SetAttrString iso. _PyObject_SetAttrId => slow path/read…
Aug 9, 2021
8a49659
Refactor _pysqlite_seterror
Aug 9, 2021
8d11bc5
Link to sqlite.org/rescode.html iso. sqlite.org/c3ref/c_abort.html
Aug 9, 2021
7a69798
Use test.support.os_helper.temp_dir() in test_error_code_on_exception()
Aug 18, 2021
b9a5a77
Merge branch 'main' into sqlite-expose-error-code
Aug 25, 2021
3589a6a
Address review: document get_exception_class() return value
Aug 25, 2021
a3c45b6
Merge branch 'main' into sqlite-expose-error-code
Aug 25, 2021
5a5683c
Address review: compare char ptr with NULL iso. 0
Aug 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion Doc/includes/sqlite3/complete_statement.py
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
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
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
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
@@ -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
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
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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slighly confusing because normally returning NULL signifies an error. Could you please document this both on the call site and in this function? Otherwise the call site reads weirdly because

    PyObject *exc_class = get_exception_class(state, errorcode);
    if (exc_class == NULL) {
        return errorcode;
    }

seems that is handling an error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I see now that it can be a bit confusing. I'll add a comment. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it clearer with 3589a6a?

(FYI, I need to rebase onto main again bco. GH-26202)

{
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