Skip to content

bpo-24139: Add support for SQLite extended result codes #28076

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

Merged
merged 23 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,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
* :mod:`sqlite3` exceptions now include the SQLite extended 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`.)
:issue:`16379` and :issue:`24139`.)


threading
---------
Expand Down
129 changes: 127 additions & 2 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def test_module_constants(self):
"SQLITE_PERM",
"SQLITE_PRAGMA",
"SQLITE_PROTOCOL",
"SQLITE_RANGE",
"SQLITE_READ",
"SQLITE_READONLY",
"SQLITE_REINDEX",
Expand All @@ -173,18 +174,142 @@ def test_module_constants(self):
if sqlite.sqlite_version_info >= (3, 8, 3):
consts.append("SQLITE_RECURSIVE")
consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"]
# Extended result codes
consts += [
"SQLITE_ABORT_ROLLBACK",
"SQLITE_BUSY_RECOVERY",
"SQLITE_CANTOPEN_FULLPATH",
"SQLITE_CANTOPEN_ISDIR",
"SQLITE_CANTOPEN_NOTEMPDIR",
"SQLITE_CORRUPT_VTAB",
"SQLITE_IOERR_ACCESS",
"SQLITE_IOERR_BLOCKED",
"SQLITE_IOERR_CHECKRESERVEDLOCK",
"SQLITE_IOERR_CLOSE",
"SQLITE_IOERR_DELETE",
"SQLITE_IOERR_DELETE_NOENT",
"SQLITE_IOERR_DIR_CLOSE",
"SQLITE_IOERR_DIR_FSYNC",
"SQLITE_IOERR_FSTAT",
"SQLITE_IOERR_FSYNC",
"SQLITE_IOERR_LOCK",
"SQLITE_IOERR_NOMEM",
"SQLITE_IOERR_RDLOCK",
"SQLITE_IOERR_READ",
"SQLITE_IOERR_SEEK",
"SQLITE_IOERR_SHMLOCK",
"SQLITE_IOERR_SHMMAP",
"SQLITE_IOERR_SHMOPEN",
"SQLITE_IOERR_SHMSIZE",
"SQLITE_IOERR_SHORT_READ",
"SQLITE_IOERR_TRUNCATE",
"SQLITE_IOERR_UNLOCK",
"SQLITE_IOERR_WRITE",
"SQLITE_LOCKED_SHAREDCACHE",
"SQLITE_READONLY_CANTLOCK",
"SQLITE_READONLY_RECOVERY",
]
if sqlite.version_info >= (3, 7, 16):
consts += [
"SQLITE_CONSTRAINT_CHECK",
"SQLITE_CONSTRAINT_COMMITHOOK",
"SQLITE_CONSTRAINT_FOREIGNKEY",
"SQLITE_CONSTRAINT_FUNCTION",
"SQLITE_CONSTRAINT_NOTNULL",
"SQLITE_CONSTRAINT_PRIMARYKEY",
"SQLITE_CONSTRAINT_TRIGGER",
"SQLITE_CONSTRAINT_UNIQUE",
"SQLITE_CONSTRAINT_VTAB",
"SQLITE_READONLY_ROLLBACK",
]
if sqlite.version_info >= (3, 7, 17):
consts += [
"SQLITE_IOERR_MMAP",
"SQLITE_NOTICE_RECOVER_ROLLBACK",
"SQLITE_NOTICE_RECOVER_WAL",
]
if sqlite.version_info >= (3, 8, 0):
consts += [
"SQLITE_BUSY_SNAPSHOT",
"SQLITE_IOERR_GETTEMPPATH",
"SQLITE_WARNING_AUTOINDEX",
]
if sqlite.version_info >= (3, 8, 1):
consts += ["SQLITE_CANTOPEN_CONVPATH", "SQLITE_IOERR_CONVPATH"]
if sqlite.version_info >= (3, 8, 2):
consts.append("SQLITE_CONSTRAINT_ROWID")
if sqlite.version_info >= (3, 8, 3):
consts.append("SQLITE_READONLY_DBMOVED")
if sqlite.version_info >= (3, 8, 7):
consts.append("SQLITE_AUTH_USER")
if sqlite.version_info >= (3, 9, 0):
consts.append("SQLITE_IOERR_VNODE")
if sqlite.version_info >= (3, 10, 0):
consts.append("SQLITE_IOERR_AUTH")
if sqlite.version_info >= (3, 14, 1):
consts.append("SQLITE_OK_LOAD_PERMANENTLY")
if sqlite.version_info >= (3, 21, 0):
consts += [
"SQLITE_IOERR_BEGIN_ATOMIC",
"SQLITE_IOERR_COMMIT_ATOMIC",
"SQLITE_IOERR_ROLLBACK_ATOMIC",
]
if sqlite.version_info >= (3, 22, 0):
consts += [
"SQLITE_ERROR_MISSING_COLLSEQ",
"SQLITE_ERROR_RETRY",
"SQLITE_READONLY_CANTINIT",
"SQLITE_READONLY_DIRECTORY",
]
if sqlite.version_info >= (3, 24, 0):
consts += ["SQLITE_CORRUPT_SEQUENCE", "SQLITE_LOCKED_VTAB"]
if sqlite.version_info >= (3, 25, 0):
consts += ["SQLITE_CANTOPEN_DIRTYWAL", "SQLITE_ERROR_SNAPSHOT"]
if sqlite.version_info >= (3, 31, 0):
consts += [
"SQLITE_CANTOPEN_SYMLINK",
"SQLITE_CONSTRAINT_PINNED",
"SQLITE_OK_SYMLINK",
]
if sqlite.version_info >= (3, 32, 0):
consts += [
"SQLITE_BUSY_TIMEOUT",
"SQLITE_CORRUPT_INDEX",
"SQLITE_IOERR_DATA",
]
if sqlite.version_info >= (3, 34, 0):
const.append("SQLITE_IOERR_CORRUPTFS")
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"
if sys.platform.startswith("win"):
err_code = sqlite.SQLITE_CANTOPEN_ISDIR
else:
err_code = sqlite.SQLITE_CANTOPEN

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")
self.assertEqual(e.sqlite_errorcode, err_code)
self.assertTrue(e.sqlite_errorname.startswith("SQLITE_CANTOPEN"))

@unittest.skipIf(sqlite.sqlite_version_info <= (3, 7, 16),
"Requires SQLite 3.7.16 or newer")
def test_extended_error_code_on_exception(self):
with managed_connect(":memory:", in_mem=True) as con:
with con:
con.execute("create table t(t integer check(t > 0))")
errmsg = "CHECK constraint failed"
with self.assertRaisesRegex(sqlite.IntegrityError, errmsg) as cm:
con.execute("insert into t values(-1)")
exc = cm.exception
self.assertEqual(exc.sqlite_errorcode,
sqlite.SQLITE_CONSTRAINT_CHECK)
self.assertEqual(exc.sqlite_errorname, "SQLITE_CONSTRAINT_CHECK")

# sqlite3_enable_shared_cache() is deprecated on macOS and calling it may raise
# OperationalError on some buildbots.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support for SQLite extended result codes in :exc:`sqlite3.Error`. Patch
by Erlend E. Aasland.
127 changes: 126 additions & 1 deletion Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,22 @@ static PyMethodDef module_methods[] = {
{NULL, NULL}
};

/* SQLite API error codes */
/* SQLite C API result codes. See also:
* - https://www.sqlite.org/c3ref/c_abort_rollback.html
* - https://sqlite.org/changes.html#version_3_3_8
* - https://sqlite.org/changes.html#version_3_7_16
* - https://sqlite.org/changes.html#version_3_7_17
* - https://sqlite.org/changes.html#version_3_8_0
* - https://sqlite.org/changes.html#version_3_8_3
* - https://sqlite.org/changes.html#version_3_14
*
* Note: the SQLite changelogs rarely mention new result codes, so in order to
* keep the 'error_codes' table in sync with SQLite, we must manually inspect
* sqlite3.h for every release.
*
* We keep the SQLITE_VERSION_NUMBER checks in order to easily declutter the
* code when we adjust the SQLite version requirement.
*/
static const struct {
const char *name;
long value;
Expand Down Expand Up @@ -311,13 +326,123 @@ static const struct {
DECLARE_ERROR_CODE(SQLITE_OK),
DECLARE_ERROR_CODE(SQLITE_PERM),
DECLARE_ERROR_CODE(SQLITE_PROTOCOL),
DECLARE_ERROR_CODE(SQLITE_RANGE),
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
// Extended result code list
DECLARE_ERROR_CODE(SQLITE_ABORT_ROLLBACK),
DECLARE_ERROR_CODE(SQLITE_BUSY_RECOVERY),
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_FULLPATH),
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_ISDIR),
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_NOTEMPDIR),
DECLARE_ERROR_CODE(SQLITE_CORRUPT_VTAB),
DECLARE_ERROR_CODE(SQLITE_IOERR_ACCESS),
DECLARE_ERROR_CODE(SQLITE_IOERR_BLOCKED),
DECLARE_ERROR_CODE(SQLITE_IOERR_CHECKRESERVEDLOCK),
DECLARE_ERROR_CODE(SQLITE_IOERR_CLOSE),
DECLARE_ERROR_CODE(SQLITE_IOERR_DELETE),
DECLARE_ERROR_CODE(SQLITE_IOERR_DELETE_NOENT),
DECLARE_ERROR_CODE(SQLITE_IOERR_DIR_CLOSE),
DECLARE_ERROR_CODE(SQLITE_IOERR_DIR_FSYNC),
DECLARE_ERROR_CODE(SQLITE_IOERR_FSTAT),
DECLARE_ERROR_CODE(SQLITE_IOERR_FSYNC),
DECLARE_ERROR_CODE(SQLITE_IOERR_LOCK),
DECLARE_ERROR_CODE(SQLITE_IOERR_NOMEM),
DECLARE_ERROR_CODE(SQLITE_IOERR_RDLOCK),
DECLARE_ERROR_CODE(SQLITE_IOERR_READ),
DECLARE_ERROR_CODE(SQLITE_IOERR_SEEK),
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMLOCK),
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMMAP),
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMOPEN),
DECLARE_ERROR_CODE(SQLITE_IOERR_SHMSIZE),
DECLARE_ERROR_CODE(SQLITE_IOERR_SHORT_READ),
DECLARE_ERROR_CODE(SQLITE_IOERR_TRUNCATE),
DECLARE_ERROR_CODE(SQLITE_IOERR_UNLOCK),
DECLARE_ERROR_CODE(SQLITE_IOERR_WRITE),
DECLARE_ERROR_CODE(SQLITE_LOCKED_SHAREDCACHE),
DECLARE_ERROR_CODE(SQLITE_READONLY_CANTLOCK),
DECLARE_ERROR_CODE(SQLITE_READONLY_RECOVERY),
#if SQLITE_VERSION_NUMBER >= 3007016
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_CHECK),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_COMMITHOOK),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_FOREIGNKEY),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_FUNCTION),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_NOTNULL),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_PRIMARYKEY),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_TRIGGER),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_UNIQUE),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_VTAB),
DECLARE_ERROR_CODE(SQLITE_READONLY_ROLLBACK),
#endif
#if SQLITE_VERSION_NUMBER >= 3007017
DECLARE_ERROR_CODE(SQLITE_IOERR_MMAP),
DECLARE_ERROR_CODE(SQLITE_NOTICE_RECOVER_ROLLBACK),
DECLARE_ERROR_CODE(SQLITE_NOTICE_RECOVER_WAL),
#endif
#if SQLITE_VERSION_NUMBER >= 3008000
DECLARE_ERROR_CODE(SQLITE_BUSY_SNAPSHOT),
DECLARE_ERROR_CODE(SQLITE_IOERR_GETTEMPPATH),
DECLARE_ERROR_CODE(SQLITE_WARNING_AUTOINDEX),
#endif
#if SQLITE_VERSION_NUMBER >= 3008001
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_CONVPATH),
DECLARE_ERROR_CODE(SQLITE_IOERR_CONVPATH),
#endif
#if SQLITE_VERSION_NUMBER >= 3008002
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_ROWID),
#endif
#if SQLITE_VERSION_NUMBER >= 3008003
DECLARE_ERROR_CODE(SQLITE_READONLY_DBMOVED),
#endif
#if SQLITE_VERSION_NUMBER >= 3008007
DECLARE_ERROR_CODE(SQLITE_AUTH_USER),
#endif
#if SQLITE_VERSION_NUMBER >= 3009000
DECLARE_ERROR_CODE(SQLITE_IOERR_VNODE),
#endif
#if SQLITE_VERSION_NUMBER >= 3010000
DECLARE_ERROR_CODE(SQLITE_IOERR_AUTH),
#endif
#if SQLITE_VERSION_NUMBER >= 3014001
DECLARE_ERROR_CODE(SQLITE_OK_LOAD_PERMANENTLY),
#endif
#if SQLITE_VERSION_NUMBER >= 3021000
DECLARE_ERROR_CODE(SQLITE_IOERR_BEGIN_ATOMIC),
DECLARE_ERROR_CODE(SQLITE_IOERR_COMMIT_ATOMIC),
DECLARE_ERROR_CODE(SQLITE_IOERR_ROLLBACK_ATOMIC),
#endif
#if SQLITE_VERSION_NUMBER >= 3022000
DECLARE_ERROR_CODE(SQLITE_ERROR_MISSING_COLLSEQ),
DECLARE_ERROR_CODE(SQLITE_ERROR_RETRY),
DECLARE_ERROR_CODE(SQLITE_READONLY_CANTINIT),
DECLARE_ERROR_CODE(SQLITE_READONLY_DIRECTORY),
#endif
#if SQLITE_VERSION_NUMBER >= 3024000
DECLARE_ERROR_CODE(SQLITE_CORRUPT_SEQUENCE),
DECLARE_ERROR_CODE(SQLITE_LOCKED_VTAB),
#endif
#if SQLITE_VERSION_NUMBER >= 3025000
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_DIRTYWAL),
DECLARE_ERROR_CODE(SQLITE_ERROR_SNAPSHOT),
#endif
#if SQLITE_VERSION_NUMBER >= 3031000
DECLARE_ERROR_CODE(SQLITE_CANTOPEN_SYMLINK),
DECLARE_ERROR_CODE(SQLITE_CONSTRAINT_PINNED),
DECLARE_ERROR_CODE(SQLITE_OK_SYMLINK),
#endif
#if SQLITE_VERSION_NUMBER >= 3032000
DECLARE_ERROR_CODE(SQLITE_BUSY_TIMEOUT),
DECLARE_ERROR_CODE(SQLITE_CORRUPT_INDEX),
DECLARE_ERROR_CODE(SQLITE_IOERR_DATA),
#endif
#if SQLITE_VERSION_NUMBER >= 3034000
DECLARE_ERROR_CODE(SQLITE_IOERR_CORRUPTFS),
#endif
#undef DECLARE_ERROR_CODE
{NULL, 0},
Expand Down
7 changes: 5 additions & 2 deletions Modules/_sqlite/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ get_exception_class(pysqlite_state *state, int errorcode)
return state->IntegrityError;
case SQLITE_MISUSE:
return state->ProgrammingError;
case SQLITE_RANGE:
return state->InterfaceError;
default:
return state->DatabaseError;
}
Expand Down Expand Up @@ -139,9 +141,10 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
}

/* Create and set the exception. */
int extended_errcode = sqlite3_extended_errcode(db);
const char *errmsg = sqlite3_errmsg(db);
raise_exception(exc_class, errorcode, errmsg);
return errorcode;
raise_exception(exc_class, extended_errcode, errmsg);
return extended_errcode;
}

#ifdef WORDS_BIGENDIAN
Expand Down