Skip to content

Commit 456e27a

Browse files
author
Erlend Egeberg Aasland
authored
bpo-24139: Add support for SQLite extended result codes (GH-28076)
1 parent a459a81 commit 456e27a

File tree

5 files changed

+263
-7
lines changed

5 files changed

+263
-7
lines changed

Doc/whatsnew/3.11.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,12 @@ sqlite3
242242
now raise :exc:`UnicodeEncodeError` instead of :exc:`sqlite3.ProgrammingError`.
243243
(Contributed by Erlend E. Aasland in :issue:`44688`.)
244244

245-
* :mod:`sqlite3` exceptions now include the SQLite error code as
245+
* :mod:`sqlite3` exceptions now include the SQLite extended error code as
246246
:attr:`~sqlite3.Error.sqlite_errorcode` and the SQLite error name as
247247
:attr:`~sqlite3.Error.sqlite_errorname`.
248248
(Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in
249-
:issue:`16379`.)
249+
:issue:`16379` and :issue:`24139`.)
250+
250251

251252
* Add :meth:`~sqlite3.Connection.setlimit` and
252253
:meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for

Lib/test/test_sqlite3/test_dbapi.py

+127-2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def test_module_constants(self):
157157
"SQLITE_PERM",
158158
"SQLITE_PRAGMA",
159159
"SQLITE_PROTOCOL",
160+
"SQLITE_RANGE",
160161
"SQLITE_READ",
161162
"SQLITE_READONLY",
162163
"SQLITE_REINDEX",
@@ -187,18 +188,142 @@ def test_module_constants(self):
187188
if sqlite.sqlite_version_info >= (3, 8, 7):
188189
consts.append("SQLITE_LIMIT_WORKER_THREADS")
189190
consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"]
191+
# Extended result codes
192+
consts += [
193+
"SQLITE_ABORT_ROLLBACK",
194+
"SQLITE_BUSY_RECOVERY",
195+
"SQLITE_CANTOPEN_FULLPATH",
196+
"SQLITE_CANTOPEN_ISDIR",
197+
"SQLITE_CANTOPEN_NOTEMPDIR",
198+
"SQLITE_CORRUPT_VTAB",
199+
"SQLITE_IOERR_ACCESS",
200+
"SQLITE_IOERR_BLOCKED",
201+
"SQLITE_IOERR_CHECKRESERVEDLOCK",
202+
"SQLITE_IOERR_CLOSE",
203+
"SQLITE_IOERR_DELETE",
204+
"SQLITE_IOERR_DELETE_NOENT",
205+
"SQLITE_IOERR_DIR_CLOSE",
206+
"SQLITE_IOERR_DIR_FSYNC",
207+
"SQLITE_IOERR_FSTAT",
208+
"SQLITE_IOERR_FSYNC",
209+
"SQLITE_IOERR_LOCK",
210+
"SQLITE_IOERR_NOMEM",
211+
"SQLITE_IOERR_RDLOCK",
212+
"SQLITE_IOERR_READ",
213+
"SQLITE_IOERR_SEEK",
214+
"SQLITE_IOERR_SHMLOCK",
215+
"SQLITE_IOERR_SHMMAP",
216+
"SQLITE_IOERR_SHMOPEN",
217+
"SQLITE_IOERR_SHMSIZE",
218+
"SQLITE_IOERR_SHORT_READ",
219+
"SQLITE_IOERR_TRUNCATE",
220+
"SQLITE_IOERR_UNLOCK",
221+
"SQLITE_IOERR_WRITE",
222+
"SQLITE_LOCKED_SHAREDCACHE",
223+
"SQLITE_READONLY_CANTLOCK",
224+
"SQLITE_READONLY_RECOVERY",
225+
]
226+
if sqlite.version_info >= (3, 7, 16):
227+
consts += [
228+
"SQLITE_CONSTRAINT_CHECK",
229+
"SQLITE_CONSTRAINT_COMMITHOOK",
230+
"SQLITE_CONSTRAINT_FOREIGNKEY",
231+
"SQLITE_CONSTRAINT_FUNCTION",
232+
"SQLITE_CONSTRAINT_NOTNULL",
233+
"SQLITE_CONSTRAINT_PRIMARYKEY",
234+
"SQLITE_CONSTRAINT_TRIGGER",
235+
"SQLITE_CONSTRAINT_UNIQUE",
236+
"SQLITE_CONSTRAINT_VTAB",
237+
"SQLITE_READONLY_ROLLBACK",
238+
]
239+
if sqlite.version_info >= (3, 7, 17):
240+
consts += [
241+
"SQLITE_IOERR_MMAP",
242+
"SQLITE_NOTICE_RECOVER_ROLLBACK",
243+
"SQLITE_NOTICE_RECOVER_WAL",
244+
]
245+
if sqlite.version_info >= (3, 8, 0):
246+
consts += [
247+
"SQLITE_BUSY_SNAPSHOT",
248+
"SQLITE_IOERR_GETTEMPPATH",
249+
"SQLITE_WARNING_AUTOINDEX",
250+
]
251+
if sqlite.version_info >= (3, 8, 1):
252+
consts += ["SQLITE_CANTOPEN_CONVPATH", "SQLITE_IOERR_CONVPATH"]
253+
if sqlite.version_info >= (3, 8, 2):
254+
consts.append("SQLITE_CONSTRAINT_ROWID")
255+
if sqlite.version_info >= (3, 8, 3):
256+
consts.append("SQLITE_READONLY_DBMOVED")
257+
if sqlite.version_info >= (3, 8, 7):
258+
consts.append("SQLITE_AUTH_USER")
259+
if sqlite.version_info >= (3, 9, 0):
260+
consts.append("SQLITE_IOERR_VNODE")
261+
if sqlite.version_info >= (3, 10, 0):
262+
consts.append("SQLITE_IOERR_AUTH")
263+
if sqlite.version_info >= (3, 14, 1):
264+
consts.append("SQLITE_OK_LOAD_PERMANENTLY")
265+
if sqlite.version_info >= (3, 21, 0):
266+
consts += [
267+
"SQLITE_IOERR_BEGIN_ATOMIC",
268+
"SQLITE_IOERR_COMMIT_ATOMIC",
269+
"SQLITE_IOERR_ROLLBACK_ATOMIC",
270+
]
271+
if sqlite.version_info >= (3, 22, 0):
272+
consts += [
273+
"SQLITE_ERROR_MISSING_COLLSEQ",
274+
"SQLITE_ERROR_RETRY",
275+
"SQLITE_READONLY_CANTINIT",
276+
"SQLITE_READONLY_DIRECTORY",
277+
]
278+
if sqlite.version_info >= (3, 24, 0):
279+
consts += ["SQLITE_CORRUPT_SEQUENCE", "SQLITE_LOCKED_VTAB"]
280+
if sqlite.version_info >= (3, 25, 0):
281+
consts += ["SQLITE_CANTOPEN_DIRTYWAL", "SQLITE_ERROR_SNAPSHOT"]
282+
if sqlite.version_info >= (3, 31, 0):
283+
consts += [
284+
"SQLITE_CANTOPEN_SYMLINK",
285+
"SQLITE_CONSTRAINT_PINNED",
286+
"SQLITE_OK_SYMLINK",
287+
]
288+
if sqlite.version_info >= (3, 32, 0):
289+
consts += [
290+
"SQLITE_BUSY_TIMEOUT",
291+
"SQLITE_CORRUPT_INDEX",
292+
"SQLITE_IOERR_DATA",
293+
]
294+
if sqlite.version_info >= (3, 34, 0):
295+
const.append("SQLITE_IOERR_CORRUPTFS")
190296
for const in consts:
191297
with self.subTest(const=const):
192298
self.assertTrue(hasattr(sqlite, const))
193299

194300
def test_error_code_on_exception(self):
195301
err_msg = "unable to open database file"
302+
if sys.platform.startswith("win"):
303+
err_code = sqlite.SQLITE_CANTOPEN_ISDIR
304+
else:
305+
err_code = sqlite.SQLITE_CANTOPEN
306+
196307
with temp_dir() as db:
197308
with self.assertRaisesRegex(sqlite.Error, err_msg) as cm:
198309
sqlite.connect(db)
199310
e = cm.exception
200-
self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN)
201-
self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN")
311+
self.assertEqual(e.sqlite_errorcode, err_code)
312+
self.assertTrue(e.sqlite_errorname.startswith("SQLITE_CANTOPEN"))
313+
314+
@unittest.skipIf(sqlite.sqlite_version_info <= (3, 7, 16),
315+
"Requires SQLite 3.7.16 or newer")
316+
def test_extended_error_code_on_exception(self):
317+
with managed_connect(":memory:", in_mem=True) as con:
318+
with con:
319+
con.execute("create table t(t integer check(t > 0))")
320+
errmsg = "CHECK constraint failed"
321+
with self.assertRaisesRegex(sqlite.IntegrityError, errmsg) as cm:
322+
con.execute("insert into t values(-1)")
323+
exc = cm.exception
324+
self.assertEqual(exc.sqlite_errorcode,
325+
sqlite.SQLITE_CONSTRAINT_CHECK)
326+
self.assertEqual(exc.sqlite_errorname, "SQLITE_CONSTRAINT_CHECK")
202327

203328
# sqlite3_enable_shared_cache() is deprecated on macOS and calling it may raise
204329
# OperationalError on some buildbots.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for SQLite extended result codes in :exc:`sqlite3.Error`. Patch
2+
by Erlend E. Aasland.

Modules/_sqlite/module.c

+126-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,22 @@ static PyMethodDef module_methods[] = {
280280
{NULL, NULL}
281281
};
282282

283-
/* SQLite API error codes */
283+
/* SQLite C API result codes. See also:
284+
* - https://www.sqlite.org/c3ref/c_abort_rollback.html
285+
* - https://sqlite.org/changes.html#version_3_3_8
286+
* - https://sqlite.org/changes.html#version_3_7_16
287+
* - https://sqlite.org/changes.html#version_3_7_17
288+
* - https://sqlite.org/changes.html#version_3_8_0
289+
* - https://sqlite.org/changes.html#version_3_8_3
290+
* - https://sqlite.org/changes.html#version_3_14
291+
*
292+
* Note: the SQLite changelogs rarely mention new result codes, so in order to
293+
* keep the 'error_codes' table in sync with SQLite, we must manually inspect
294+
* sqlite3.h for every release.
295+
*
296+
* We keep the SQLITE_VERSION_NUMBER checks in order to easily declutter the
297+
* code when we adjust the SQLite version requirement.
298+
*/
284299
static const struct {
285300
const char *name;
286301
long value;
@@ -311,13 +326,123 @@ static const struct {
311326
DECLARE_ERROR_CODE(SQLITE_OK),
312327
DECLARE_ERROR_CODE(SQLITE_PERM),
313328
DECLARE_ERROR_CODE(SQLITE_PROTOCOL),
329+
DECLARE_ERROR_CODE(SQLITE_RANGE),
314330
DECLARE_ERROR_CODE(SQLITE_READONLY),
315331
DECLARE_ERROR_CODE(SQLITE_ROW),
316332
DECLARE_ERROR_CODE(SQLITE_SCHEMA),
317333
DECLARE_ERROR_CODE(SQLITE_TOOBIG),
318334
#if SQLITE_VERSION_NUMBER >= 3007017
319335
DECLARE_ERROR_CODE(SQLITE_NOTICE),
320336
DECLARE_ERROR_CODE(SQLITE_WARNING),
337+
#endif
338+
// Extended result code list
339+
DECLARE_ERROR_CODE(SQLITE_ABORT_ROLLBACK),
340+
DECLARE_ERROR_CODE(SQLITE_BUSY_RECOVERY),
341+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_FULLPATH),
342+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_ISDIR),
343+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_NOTEMPDIR),
344+
DECLARE_ERROR_CODE(SQLITE_CORRUPT_VTAB),
345+
DECLARE_ERROR_CODE(SQLITE_IOERR_ACCESS),
346+
DECLARE_ERROR_CODE(SQLITE_IOERR_BLOCKED),
347+
DECLARE_ERROR_CODE(SQLITE_IOERR_CHECKRESERVEDLOCK),
348+
DECLARE_ERROR_CODE(SQLITE_IOERR_CLOSE),
349+
DECLARE_ERROR_CODE(SQLITE_IOERR_DELETE),
350+
DECLARE_ERROR_CODE(SQLITE_IOERR_DELETE_NOENT),
351+
DECLARE_ERROR_CODE(SQLITE_IOERR_DIR_CLOSE),
352+
DECLARE_ERROR_CODE(SQLITE_IOERR_DIR_FSYNC),
353+
DECLARE_ERROR_CODE(SQLITE_IOERR_FSTAT),
354+
DECLARE_ERROR_CODE(SQLITE_IOERR_FSYNC),
355+
DECLARE_ERROR_CODE(SQLITE_IOERR_LOCK),
356+
DECLARE_ERROR_CODE(SQLITE_IOERR_NOMEM),
357+
DECLARE_ERROR_CODE(SQLITE_IOERR_RDLOCK),
358+
DECLARE_ERROR_CODE(SQLITE_IOERR_READ),
359+
DECLARE_ERROR_CODE(SQLITE_IOERR_SEEK),
360+
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMLOCK),
361+
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMMAP),
362+
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMOPEN),
363+
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMSIZE),
364+
DECLARE_ERROR_CODE(SQLITE_IOERR_SHORT_READ),
365+
DECLARE_ERROR_CODE(SQLITE_IOERR_TRUNCATE),
366+
DECLARE_ERROR_CODE(SQLITE_IOERR_UNLOCK),
367+
DECLARE_ERROR_CODE(SQLITE_IOERR_WRITE),
368+
DECLARE_ERROR_CODE(SQLITE_LOCKED_SHAREDCACHE),
369+
DECLARE_ERROR_CODE(SQLITE_READONLY_CANTLOCK),
370+
DECLARE_ERROR_CODE(SQLITE_READONLY_RECOVERY),
371+
#if SQLITE_VERSION_NUMBER >= 3007016
372+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_CHECK),
373+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_COMMITHOOK),
374+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_FOREIGNKEY),
375+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_FUNCTION),
376+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_NOTNULL),
377+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_PRIMARYKEY),
378+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_TRIGGER),
379+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_UNIQUE),
380+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_VTAB),
381+
DECLARE_ERROR_CODE(SQLITE_READONLY_ROLLBACK),
382+
#endif
383+
#if SQLITE_VERSION_NUMBER >= 3007017
384+
DECLARE_ERROR_CODE(SQLITE_IOERR_MMAP),
385+
DECLARE_ERROR_CODE(SQLITE_NOTICE_RECOVER_ROLLBACK),
386+
DECLARE_ERROR_CODE(SQLITE_NOTICE_RECOVER_WAL),
387+
#endif
388+
#if SQLITE_VERSION_NUMBER >= 3008000
389+
DECLARE_ERROR_CODE(SQLITE_BUSY_SNAPSHOT),
390+
DECLARE_ERROR_CODE(SQLITE_IOERR_GETTEMPPATH),
391+
DECLARE_ERROR_CODE(SQLITE_WARNING_AUTOINDEX),
392+
#endif
393+
#if SQLITE_VERSION_NUMBER >= 3008001
394+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_CONVPATH),
395+
DECLARE_ERROR_CODE(SQLITE_IOERR_CONVPATH),
396+
#endif
397+
#if SQLITE_VERSION_NUMBER >= 3008002
398+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_ROWID),
399+
#endif
400+
#if SQLITE_VERSION_NUMBER >= 3008003
401+
DECLARE_ERROR_CODE(SQLITE_READONLY_DBMOVED),
402+
#endif
403+
#if SQLITE_VERSION_NUMBER >= 3008007
404+
DECLARE_ERROR_CODE(SQLITE_AUTH_USER),
405+
#endif
406+
#if SQLITE_VERSION_NUMBER >= 3009000
407+
DECLARE_ERROR_CODE(SQLITE_IOERR_VNODE),
408+
#endif
409+
#if SQLITE_VERSION_NUMBER >= 3010000
410+
DECLARE_ERROR_CODE(SQLITE_IOERR_AUTH),
411+
#endif
412+
#if SQLITE_VERSION_NUMBER >= 3014001
413+
DECLARE_ERROR_CODE(SQLITE_OK_LOAD_PERMANENTLY),
414+
#endif
415+
#if SQLITE_VERSION_NUMBER >= 3021000
416+
DECLARE_ERROR_CODE(SQLITE_IOERR_BEGIN_ATOMIC),
417+
DECLARE_ERROR_CODE(SQLITE_IOERR_COMMIT_ATOMIC),
418+
DECLARE_ERROR_CODE(SQLITE_IOERR_ROLLBACK_ATOMIC),
419+
#endif
420+
#if SQLITE_VERSION_NUMBER >= 3022000
421+
DECLARE_ERROR_CODE(SQLITE_ERROR_MISSING_COLLSEQ),
422+
DECLARE_ERROR_CODE(SQLITE_ERROR_RETRY),
423+
DECLARE_ERROR_CODE(SQLITE_READONLY_CANTINIT),
424+
DECLARE_ERROR_CODE(SQLITE_READONLY_DIRECTORY),
425+
#endif
426+
#if SQLITE_VERSION_NUMBER >= 3024000
427+
DECLARE_ERROR_CODE(SQLITE_CORRUPT_SEQUENCE),
428+
DECLARE_ERROR_CODE(SQLITE_LOCKED_VTAB),
429+
#endif
430+
#if SQLITE_VERSION_NUMBER >= 3025000
431+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_DIRTYWAL),
432+
DECLARE_ERROR_CODE(SQLITE_ERROR_SNAPSHOT),
433+
#endif
434+
#if SQLITE_VERSION_NUMBER >= 3031000
435+
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_SYMLINK),
436+
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_PINNED),
437+
DECLARE_ERROR_CODE(SQLITE_OK_SYMLINK),
438+
#endif
439+
#if SQLITE_VERSION_NUMBER >= 3032000
440+
DECLARE_ERROR_CODE(SQLITE_BUSY_TIMEOUT),
441+
DECLARE_ERROR_CODE(SQLITE_CORRUPT_INDEX),
442+
DECLARE_ERROR_CODE(SQLITE_IOERR_DATA),
443+
#endif
444+
#if SQLITE_VERSION_NUMBER >= 3034000
445+
DECLARE_ERROR_CODE(SQLITE_IOERR_CORRUPTFS),
321446
#endif
322447
#undef DECLARE_ERROR_CODE
323448
{NULL, 0},

Modules/_sqlite/util.c

+5-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ get_exception_class(pysqlite_state *state, int errorcode)
7272
return state->IntegrityError;
7373
case SQLITE_MISUSE:
7474
return state->ProgrammingError;
75+
case SQLITE_RANGE:
76+
return state->InterfaceError;
7577
default:
7678
return state->DatabaseError;
7779
}
@@ -139,9 +141,10 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
139141
}
140142

141143
/* Create and set the exception. */
144+
int extended_errcode = sqlite3_extended_errcode(db);
142145
const char *errmsg = sqlite3_errmsg(db);
143-
raise_exception(exc_class, errorcode, errmsg);
144-
return errorcode;
146+
raise_exception(exc_class, extended_errcode, errmsg);
147+
return extended_errcode;
145148
}
146149

147150
#ifdef WORDS_BIGENDIAN

0 commit comments

Comments
 (0)