Skip to content

Commit 86d8b46

Browse files
author
Erlend Egeberg Aasland
authored
bpo-16379: expose SQLite error codes and error names in sqlite3 (GH-27786)
1 parent f62763d commit 86d8b46

File tree

8 files changed

+264
-32
lines changed

8 files changed

+264
-32
lines changed

Doc/includes/sqlite3/complete_statement.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
if buffer.lstrip().upper().startswith("SELECT"):
2525
print(cur.fetchall())
2626
except sqlite3.Error as e:
27-
print("An error occurred:", e.args[0])
27+
err_msg = str(e)
28+
err_code = e.sqlite_errorcode
29+
err_name = e.sqlite_errorname
30+
print(f"{err_name} ({err_code}): {err_msg}")
2831
buffer = ""
2932

3033
con.close()

Doc/library/sqlite3.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,20 @@ Exceptions
836836
The base class of the other exceptions in this module. It is a subclass
837837
of :exc:`Exception`.
838838

839+
.. attribute:: sqlite_errorcode
840+
841+
The numeric error code from the
842+
`SQLite API <https://sqlite.org/rescode.html>`_
843+
844+
.. versionadded:: 3.11
845+
846+
.. attribute:: sqlite_errorname
847+
848+
The symbolic name of the numeric error code
849+
from the `SQLite API <https://sqlite.org/rescode.html>`_
850+
851+
.. versionadded:: 3.11
852+
839853
.. exception:: DatabaseError
840854

841855
Exception raised for errors that are related to the database.

Doc/whatsnew/3.11.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ sqlite3
226226
now raise :exc:`UnicodeEncodeError` instead of :exc:`sqlite3.ProgrammingError`.
227227
(Contributed by Erlend E. Aasland in :issue:`44688`.)
228228

229+
* :mod:`sqlite3` exceptions now include the SQLite error code as
230+
:attr:`~sqlite3.Error.sqlite_errorcode` and the SQLite error name as
231+
:attr:`~sqlite3.Error.sqlite_errorname`.
232+
(Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in
233+
:issue:`16379`.)
234+
229235

230236
Removed
231237
=======

Lib/sqlite3/test/dbapi.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
import unittest
2929

3030
from test.support import (
31+
SHORT_TIMEOUT,
3132
bigmemtest,
3233
check_disallow_instantiation,
3334
threading_helper,
34-
SHORT_TIMEOUT,
3535
)
36-
from test.support.os_helper import TESTFN, unlink
36+
from test.support.os_helper import TESTFN, unlink, temp_dir
3737

3838

3939
# Helper for tests using TESTFN
@@ -102,6 +102,89 @@ def test_not_supported_error(self):
102102
sqlite.DatabaseError),
103103
"NotSupportedError is not a subclass of DatabaseError")
104104

105+
def test_module_constants(self):
106+
consts = [
107+
"SQLITE_ABORT",
108+
"SQLITE_ALTER_TABLE",
109+
"SQLITE_ANALYZE",
110+
"SQLITE_ATTACH",
111+
"SQLITE_AUTH",
112+
"SQLITE_BUSY",
113+
"SQLITE_CANTOPEN",
114+
"SQLITE_CONSTRAINT",
115+
"SQLITE_CORRUPT",
116+
"SQLITE_CREATE_INDEX",
117+
"SQLITE_CREATE_TABLE",
118+
"SQLITE_CREATE_TEMP_INDEX",
119+
"SQLITE_CREATE_TEMP_TABLE",
120+
"SQLITE_CREATE_TEMP_TRIGGER",
121+
"SQLITE_CREATE_TEMP_VIEW",
122+
"SQLITE_CREATE_TRIGGER",
123+
"SQLITE_CREATE_VIEW",
124+
"SQLITE_CREATE_VTABLE",
125+
"SQLITE_DELETE",
126+
"SQLITE_DENY",
127+
"SQLITE_DETACH",
128+
"SQLITE_DONE",
129+
"SQLITE_DROP_INDEX",
130+
"SQLITE_DROP_TABLE",
131+
"SQLITE_DROP_TEMP_INDEX",
132+
"SQLITE_DROP_TEMP_TABLE",
133+
"SQLITE_DROP_TEMP_TRIGGER",
134+
"SQLITE_DROP_TEMP_VIEW",
135+
"SQLITE_DROP_TRIGGER",
136+
"SQLITE_DROP_VIEW",
137+
"SQLITE_DROP_VTABLE",
138+
"SQLITE_EMPTY",
139+
"SQLITE_ERROR",
140+
"SQLITE_FORMAT",
141+
"SQLITE_FULL",
142+
"SQLITE_FUNCTION",
143+
"SQLITE_IGNORE",
144+
"SQLITE_INSERT",
145+
"SQLITE_INTERNAL",
146+
"SQLITE_INTERRUPT",
147+
"SQLITE_IOERR",
148+
"SQLITE_LOCKED",
149+
"SQLITE_MISMATCH",
150+
"SQLITE_MISUSE",
151+
"SQLITE_NOLFS",
152+
"SQLITE_NOMEM",
153+
"SQLITE_NOTADB",
154+
"SQLITE_NOTFOUND",
155+
"SQLITE_OK",
156+
"SQLITE_PERM",
157+
"SQLITE_PRAGMA",
158+
"SQLITE_PROTOCOL",
159+
"SQLITE_READ",
160+
"SQLITE_READONLY",
161+
"SQLITE_REINDEX",
162+
"SQLITE_ROW",
163+
"SQLITE_SAVEPOINT",
164+
"SQLITE_SCHEMA",
165+
"SQLITE_SELECT",
166+
"SQLITE_TOOBIG",
167+
"SQLITE_TRANSACTION",
168+
"SQLITE_UPDATE",
169+
]
170+
if sqlite.version_info >= (3, 7, 17):
171+
consts += ["SQLITE_NOTICE", "SQLITE_WARNING"]
172+
if sqlite.version_info >= (3, 8, 3):
173+
consts.append("SQLITE_RECURSIVE")
174+
consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"]
175+
for const in consts:
176+
with self.subTest(const=const):
177+
self.assertTrue(hasattr(sqlite, const))
178+
179+
def test_error_code_on_exception(self):
180+
err_msg = "unable to open database file"
181+
with temp_dir() as db:
182+
with self.assertRaisesRegex(sqlite.Error, err_msg) as cm:
183+
sqlite.connect(db)
184+
e = cm.exception
185+
self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN)
186+
self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN")
187+
105188
# sqlite3_enable_shared_cache() is deprecated on macOS and calling it may raise
106189
# OperationalError on some buildbots.
107190
@unittest.skipIf(sys.platform == "darwin", "shared cache is deprecated on macOS")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add SQLite error code and name to :mod:`sqlite3` exceptions.
2+
Patch by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland.

Modules/_sqlite/module.c

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,79 @@ static PyMethodDef module_methods[] = {
282282
{NULL, NULL}
283283
};
284284

285+
/* SQLite API error codes */
286+
static const struct {
287+
const char *name;
288+
long value;
289+
} error_codes[] = {
290+
#define DECLARE_ERROR_CODE(code) {#code, code}
291+
// Primary result code list
292+
DECLARE_ERROR_CODE(SQLITE_ABORT),
293+
DECLARE_ERROR_CODE(SQLITE_AUTH),
294+
DECLARE_ERROR_CODE(SQLITE_BUSY),
295+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN),
296+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT),
297+
DECLARE_ERROR_CODE(SQLITE_CORRUPT),
298+
DECLARE_ERROR_CODE(SQLITE_DONE),
299+
DECLARE_ERROR_CODE(SQLITE_EMPTY),
300+
DECLARE_ERROR_CODE(SQLITE_ERROR),
301+
DECLARE_ERROR_CODE(SQLITE_FORMAT),
302+
DECLARE_ERROR_CODE(SQLITE_FULL),
303+
DECLARE_ERROR_CODE(SQLITE_INTERNAL),
304+
DECLARE_ERROR_CODE(SQLITE_INTERRUPT),
305+
DECLARE_ERROR_CODE(SQLITE_IOERR),
306+
DECLARE_ERROR_CODE(SQLITE_LOCKED),
307+
DECLARE_ERROR_CODE(SQLITE_MISMATCH),
308+
DECLARE_ERROR_CODE(SQLITE_MISUSE),
309+
DECLARE_ERROR_CODE(SQLITE_NOLFS),
310+
DECLARE_ERROR_CODE(SQLITE_NOMEM),
311+
DECLARE_ERROR_CODE(SQLITE_NOTADB),
312+
DECLARE_ERROR_CODE(SQLITE_NOTFOUND),
313+
DECLARE_ERROR_CODE(SQLITE_OK),
314+
DECLARE_ERROR_CODE(SQLITE_PERM),
315+
DECLARE_ERROR_CODE(SQLITE_PROTOCOL),
316+
DECLARE_ERROR_CODE(SQLITE_READONLY),
317+
DECLARE_ERROR_CODE(SQLITE_ROW),
318+
DECLARE_ERROR_CODE(SQLITE_SCHEMA),
319+
DECLARE_ERROR_CODE(SQLITE_TOOBIG),
320+
#if SQLITE_VERSION_NUMBER >= 3007017
321+
DECLARE_ERROR_CODE(SQLITE_NOTICE),
322+
DECLARE_ERROR_CODE(SQLITE_WARNING),
323+
#endif
324+
#undef DECLARE_ERROR_CODE
325+
{NULL, 0},
326+
};
327+
328+
static int
329+
add_error_constants(PyObject *module)
330+
{
331+
for (int i = 0; error_codes[i].name != NULL; i++) {
332+
const char *name = error_codes[i].name;
333+
const long value = error_codes[i].value;
334+
if (PyModule_AddIntConstant(module, name, value) < 0) {
335+
return -1;
336+
}
337+
}
338+
return 0;
339+
}
340+
341+
const char *
342+
pysqlite_error_name(int rc)
343+
{
344+
for (int i = 0; error_codes[i].name != NULL; i++) {
345+
if (error_codes[i].value == rc) {
346+
return error_codes[i].name;
347+
}
348+
}
349+
// No error code matched.
350+
return NULL;
351+
}
352+
285353
static int add_integer_constants(PyObject *module) {
286354
int ret = 0;
287355

288356
ret += PyModule_AddIntMacro(module, PARSE_DECLTYPES);
289357
ret += PyModule_AddIntMacro(module, PARSE_COLNAMES);
290-
ret += PyModule_AddIntMacro(module, SQLITE_OK);
291358
ret += PyModule_AddIntMacro(module, SQLITE_DENY);
292359
ret += PyModule_AddIntMacro(module, SQLITE_IGNORE);
293360
ret += PyModule_AddIntMacro(module, SQLITE_CREATE_INDEX);
@@ -325,7 +392,6 @@ static int add_integer_constants(PyObject *module) {
325392
#if SQLITE_VERSION_NUMBER >= 3008003
326393
ret += PyModule_AddIntMacro(module, SQLITE_RECURSIVE);
327394
#endif
328-
ret += PyModule_AddIntMacro(module, SQLITE_DONE);
329395
return ret;
330396
}
331397

@@ -406,6 +472,11 @@ PyMODINIT_FUNC PyInit__sqlite3(void)
406472
ADD_EXCEPTION(module, state, DataError, state->DatabaseError);
407473
ADD_EXCEPTION(module, state, NotSupportedError, state->DatabaseError);
408474

475+
/* Set error constants */
476+
if (add_error_constants(module) < 0) {
477+
goto error;
478+
}
479+
409480
/* Set integer constants */
410481
if (add_integer_constants(module) < 0) {
411482
goto error;

Modules/_sqlite/module.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pysqlite_get_state_by_type(PyTypeObject *Py_UNUSED(tp))
8181
return &pysqlite_global_state;
8282
}
8383

84+
extern const char *pysqlite_error_name(int rc);
85+
8486
#define PARSE_DECLTYPES 1
8587
#define PARSE_COLNAMES 2
8688
#endif

Modules/_sqlite/util.c

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,19 @@ pysqlite_step(sqlite3_stmt *statement)
3636
return rc;
3737
}
3838

39-
/**
40-
* Checks the SQLite error code and sets the appropriate DB-API exception.
41-
* Returns the error code (0 means no error occurred).
42-
*/
43-
int
44-
_pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
39+
// Returns non-NULL if a new exception should be raised
40+
static PyObject *
41+
get_exception_class(pysqlite_state *state, int errorcode)
4542
{
46-
int errorcode = sqlite3_errcode(db);
47-
48-
switch (errorcode)
49-
{
43+
switch (errorcode) {
5044
case SQLITE_OK:
5145
PyErr_Clear();
52-
break;
46+
return NULL;
5347
case SQLITE_INTERNAL:
5448
case SQLITE_NOTFOUND:
55-
PyErr_SetString(state->InternalError, sqlite3_errmsg(db));
56-
break;
49+
return state->InternalError;
5750
case SQLITE_NOMEM:
58-
(void)PyErr_NoMemory();
59-
break;
51+
return PyErr_NoMemory();
6052
case SQLITE_ERROR:
6153
case SQLITE_PERM:
6254
case SQLITE_ABORT:
@@ -70,26 +62,85 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
7062
case SQLITE_PROTOCOL:
7163
case SQLITE_EMPTY:
7264
case SQLITE_SCHEMA:
73-
PyErr_SetString(state->OperationalError, sqlite3_errmsg(db));
74-
break;
65+
return state->OperationalError;
7566
case SQLITE_CORRUPT:
76-
PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db));
77-
break;
67+
return state->DatabaseError;
7868
case SQLITE_TOOBIG:
79-
PyErr_SetString(state->DataError, sqlite3_errmsg(db));
80-
break;
69+
return state->DataError;
8170
case SQLITE_CONSTRAINT:
8271
case SQLITE_MISMATCH:
83-
PyErr_SetString(state->IntegrityError, sqlite3_errmsg(db));
84-
break;
72+
return state->IntegrityError;
8573
case SQLITE_MISUSE:
86-
PyErr_SetString(state->ProgrammingError, sqlite3_errmsg(db));
87-
break;
74+
return state->ProgrammingError;
8875
default:
89-
PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db));
90-
break;
76+
return state->DatabaseError;
77+
}
78+
}
79+
80+
static void
81+
raise_exception(PyObject *type, int errcode, const char *errmsg)
82+
{
83+
PyObject *exc = NULL;
84+
PyObject *args[] = { PyUnicode_FromString(errmsg), };
85+
if (args[0] == NULL) {
86+
goto exit;
87+
}
88+
exc = PyObject_Vectorcall(type, args, 1, NULL);
89+
Py_DECREF(args[0]);
90+
if (exc == NULL) {
91+
goto exit;
92+
}
93+
94+
PyObject *code = PyLong_FromLong(errcode);
95+
if (code == NULL) {
96+
goto exit;
97+
}
98+
int rc = PyObject_SetAttrString(exc, "sqlite_errorcode", code);
99+
Py_DECREF(code);
100+
if (rc < 0) {
101+
goto exit;
102+
}
103+
104+
const char *error_name = pysqlite_error_name(errcode);
105+
PyObject *name;
106+
if (error_name) {
107+
name = PyUnicode_FromString(error_name);
108+
}
109+
else {
110+
name = PyUnicode_InternFromString("unknown");
111+
}
112+
if (name == NULL) {
113+
goto exit;
114+
}
115+
rc = PyObject_SetAttrString(exc, "sqlite_errorname", name);
116+
Py_DECREF(name);
117+
if (rc < 0) {
118+
goto exit;
119+
}
120+
121+
PyErr_SetObject(type, exc);
122+
123+
exit:
124+
Py_XDECREF(exc);
125+
}
126+
127+
/**
128+
* Checks the SQLite error code and sets the appropriate DB-API exception.
129+
* Returns the error code (0 means no error occurred).
130+
*/
131+
int
132+
_pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
133+
{
134+
int errorcode = sqlite3_errcode(db);
135+
PyObject *exc_class = get_exception_class(state, errorcode);
136+
if (exc_class == NULL) {
137+
// No new exception need be raised; just pass the error code
138+
return errorcode;
91139
}
92140

141+
/* Create and set the exception. */
142+
const char *errmsg = sqlite3_errmsg(db);
143+
raise_exception(exc_class, errorcode, errmsg);
93144
return errorcode;
94145
}
95146

0 commit comments

Comments
 (0)