From 456e27ac0ac6bc1cfd6da0191bd7802d8667457b Mon Sep 17 00:00:00 2001 From: Erlend Egeberg Aasland Date: Wed, 3 Nov 2021 00:49:38 +0100 Subject: [PATCH] bpo-24139: Add support for SQLite extended result codes (GH-28076) --- Doc/whatsnew/3.11.rst | 5 +- Lib/test/test_sqlite3/test_dbapi.py | 129 +++++++++++++++++- .../2021-08-30-23-10-48.bpo-24139.e38czf.rst | 2 + Modules/_sqlite/module.c | 127 ++++++++++++++++- Modules/_sqlite/util.c | 7 +- 5 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-08-30-23-10-48.bpo-24139.e38czf.rst diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 3500b1ba6b1..26d0dbb32af 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -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`.) + * Add :meth:`~sqlite3.Connection.setlimit` and :meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 34895be0180..0ba313d9298 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -157,6 +157,7 @@ class ModuleTests(unittest.TestCase): "SQLITE_PERM", "SQLITE_PRAGMA", "SQLITE_PROTOCOL", + "SQLITE_RANGE", "SQLITE_READ", "SQLITE_READONLY", "SQLITE_REINDEX", @@ -187,18 +188,142 @@ class ModuleTests(unittest.TestCase): if sqlite.sqlite_version_info >= (3, 8, 7): consts.append("SQLITE_LIMIT_WORKER_THREADS") 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. diff --git a/Misc/NEWS.d/next/Library/2021-08-30-23-10-48.bpo-24139.e38czf.rst b/Misc/NEWS.d/next/Library/2021-08-30-23-10-48.bpo-24139.e38czf.rst new file mode 100644 index 00000000000..b44d0cf6e20 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-08-30-23-10-48.bpo-24139.e38czf.rst @@ -0,0 +1,2 @@ +Add support for SQLite extended result codes in :exc:`sqlite3.Error`. Patch +by Erlend E. Aasland. diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 65229623b84..3bca33b8e8c 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -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; @@ -311,6 +326,7 @@ 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), @@ -318,6 +334,115 @@ static const struct { #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}, diff --git a/Modules/_sqlite/util.c b/Modules/_sqlite/util.c index cfd189dfc33..113b581bfac 100644 --- a/Modules/_sqlite/util.c +++ b/Modules/_sqlite/util.c @@ -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; } @@ -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