From e32c3729d318dc9dde5ebfd6a8f7a10fe2848ea5 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sat, 4 Jan 2025 16:41:26 -0800 Subject: [PATCH 01/13] sqlite3_file_control --- Lib/test/test_sqlite3/test_dbapi.py | 27 ++++++ ...-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst | 1 + Modules/_sqlite/clinic/connection.c.h | 93 ++++++++++++++++++- Modules/_sqlite/connection.c | 40 ++++++++ Modules/_sqlite/module.c | 42 +++++++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 488b401fb0054d..7845cdf1b23b32 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -25,6 +25,7 @@ import sqlite3 as sqlite import subprocess import sys +import tempfile import threading import unittest import urllib.parse @@ -735,6 +736,30 @@ def test_database_keyword(self): with contextlib.closing(sqlite.connect(database=":memory:")) as cx: self.assertEqual(type(cx), sqlite.Connection) + def test_wal_preservation(self): + with tempfile.TemporaryDirectory() as dirname: + path = os.path.join(dirname, "db.sqlite") + with contextlib.closing(sqlite.connect(path)) as cx: + cx.set_file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + cu = cx.cursor() + cu.execute("PRAGMA journal_mode = WAL") + cu.execute("CREATE TABLE foo (id int)") + cu.execute("INSERT INTO foo (id) VALUES (1)") + self.assertTrue(os.path.exists(path + "-wal")) + self.assertTrue(os.path.exists(path + "-wal")) + + with contextlib.closing(sqlite.connect(path)) as cx: + cu = cx.cursor() + self.assertTrue(os.path.exists(path + "-wal")) + cu.execute("INSERT INTO foo (id) VALUES (2)") + self.assertFalse(os.path.exists(path + "-wal")) + + + def test_file_control_raises(self): + with contextlib.closing(sqlite.connect(database=":memory:")) as cx: + with self.assertRaises(sqlite.ProgrammingError): + cx.set_file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + class CursorTests(unittest.TestCase): def setUp(self): @@ -1863,6 +1888,8 @@ def test_on_conflict_replace(self): self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')]) + + @requires_subprocess() class MultiprocessTests(unittest.TestCase): CONNECTION_TIMEOUT = 0 # Disable the busy timeout. diff --git a/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst new file mode 100644 index 00000000000000..cb700765fd797a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst @@ -0,0 +1 @@ +sqlite Connection objects now expose a method set_file_control, which is a thin wrapper for `sqlite3_file_control https://www.sqlite.org/c3ref/file_control.html`_. diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 42eb6eb2f12554..9d96e1bbd93f29 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1455,6 +1455,97 @@ pysqlite_connection_create_collation(pysqlite_Connection *self, PyTypeObject *cl return return_value; } +PyDoc_STRVAR(pysqlite_connection_set_file_control__doc__, +"set_file_control($self, op, arg, /, dbname=)\n" +"--\n" +"\n" +"Invoke a file control method on the database.\n" +"\n" +" op\n" +" a SQLITE_FCNTL_ constant\n" +" arg\n" +" argument to pass\n" +" dbname\n" +" database name"); + +#define PYSQLITE_CONNECTION_SET_FILE_CONTROL_METHODDEF \ + {"set_file_control", _PyCFunction_CAST(pysqlite_connection_set_file_control), METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_set_file_control__doc__}, + +static PyObject * +pysqlite_connection_set_file_control_impl(pysqlite_Connection *self, int op, + long arg, const char *dbname); + +static PyObject * +pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(dbname), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "", "dbname", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "set_file_control", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + int op; + long arg; + const char *dbname = NULL; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 2, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + op = PyLong_AsInt(args[0]); + if (op == -1 && PyErr_Occurred()) { + goto exit; + } + arg = PyLong_AsLong(args[1]); + if (arg == -1 && PyErr_Occurred()) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + if (!PyUnicode_Check(args[2])) { + _PyArg_BadArgument("set_file_control", "argument 'dbname'", "str", args[2]); + goto exit; + } + Py_ssize_t dbname_length; + dbname = PyUnicode_AsUTF8AndSize(args[2], &dbname_length); + if (dbname == NULL) { + goto exit; + } + if (strlen(dbname) != (size_t)dbname_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } +skip_optional_pos: + return_value = pysqlite_connection_set_file_control_impl(self, op, arg, dbname); + +exit: + return return_value; +} + #if defined(PY_SQLITE_HAVE_SERIALIZE) PyDoc_STRVAR(serialize__doc__, @@ -1881,4 +1972,4 @@ getconfig(pysqlite_Connection *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=a8fd19301c7390cc input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3465ff17f7ac6104 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index fc03e4a085c179..d316746633725d 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2173,6 +2173,45 @@ pysqlite_connection_create_collation_impl(pysqlite_Connection *self, Py_RETURN_NONE; } +/*[clinic input] +_sqlite3.Connection.set_file_control as pysqlite_connection_set_file_control + + op: int + a SQLITE_FCNTL_ constant + arg: long + argument to pass + / + dbname: str = NULL + database name + +Invoke a file control method on the database. +[clinic start generated code]*/ + +static PyObject * +pysqlite_connection_set_file_control_impl(pysqlite_Connection *self, int op, + long arg, const char *dbname) +/*[clinic end generated code: output=d9d2d311892893b6 input=0253798d9514fea2]*/ +{ + int rc; + long val = arg; + + if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_file_control(self->db, dbname, op, &val); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + PyErr_SetString(self->ProgrammingError, sqlite3_errstr(rc)); + return NULL; + } + + return PyLong_FromLong(val); +} + + #ifdef PY_SQLITE_HAVE_SERIALIZE /*[clinic input] _sqlite3.Connection.serialize as serialize @@ -2601,6 +2640,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_SET_AUTHORIZER_METHODDEF PYSQLITE_CONNECTION_SET_PROGRESS_HANDLER_METHODDEF PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF + PYSQLITE_CONNECTION_SET_FILE_CONTROL_METHODDEF SETLIMIT_METHODDEF GETLIMIT_METHODDEF SERIALIZE_METHODDEF diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 698e81d9b897d0..c9db22e2228139 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -514,6 +514,48 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_DBCONFIG_LEGACY_FILE_FORMAT); ADD_INT(SQLITE_DBCONFIG_TRUSTED_SCHEMA); #endif + ADD_INT(SQLITE_FCNTL_LOCKSTATE); + ADD_INT(SQLITE_FCNTL_GET_LOCKPROXYFILE); + ADD_INT(SQLITE_FCNTL_SET_LOCKPROXYFILE); + ADD_INT(SQLITE_FCNTL_LAST_ERRNO); + ADD_INT(SQLITE_FCNTL_SIZE_HINT); + ADD_INT(SQLITE_FCNTL_CHUNK_SIZE); + ADD_INT(SQLITE_FCNTL_FILE_POINTER); + ADD_INT(SQLITE_FCNTL_SYNC_OMITTED); + ADD_INT(SQLITE_FCNTL_WIN32_AV_RETRY); + ADD_INT(SQLITE_FCNTL_PERSIST_WAL); + ADD_INT(SQLITE_FCNTL_OVERWRITE); + ADD_INT(SQLITE_FCNTL_VFSNAME); + ADD_INT(SQLITE_FCNTL_POWERSAFE_OVERWRITE); + ADD_INT(SQLITE_FCNTL_PRAGMA); + ADD_INT(SQLITE_FCNTL_BUSYHANDLER); + ADD_INT(SQLITE_FCNTL_TEMPFILENAME); + ADD_INT(SQLITE_FCNTL_MMAP_SIZE); + ADD_INT(SQLITE_FCNTL_TRACE); + ADD_INT(SQLITE_FCNTL_HAS_MOVED); + ADD_INT(SQLITE_FCNTL_SYNC); + ADD_INT(SQLITE_FCNTL_COMMIT_PHASETWO); + ADD_INT(SQLITE_FCNTL_WIN32_SET_HANDLE); + ADD_INT(SQLITE_FCNTL_WAL_BLOCK); + ADD_INT(SQLITE_FCNTL_ZIPVFS); + ADD_INT(SQLITE_FCNTL_RBU); + ADD_INT(SQLITE_FCNTL_VFS_POINTER); + ADD_INT(SQLITE_FCNTL_JOURNAL_POINTER); + ADD_INT(SQLITE_FCNTL_WIN32_GET_HANDLE); + ADD_INT(SQLITE_FCNTL_PDB); + ADD_INT(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE); + ADD_INT(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE); + ADD_INT(SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE); + ADD_INT(SQLITE_FCNTL_LOCK_TIMEOUT); + ADD_INT(SQLITE_FCNTL_DATA_VERSION); + ADD_INT(SQLITE_FCNTL_SIZE_LIMIT); + ADD_INT(SQLITE_FCNTL_CKPT_DONE); + ADD_INT(SQLITE_FCNTL_RESERVE_BYTES); + ADD_INT(SQLITE_FCNTL_CKPT_START); + ADD_INT(SQLITE_FCNTL_EXTERNAL_READER); + ADD_INT(SQLITE_FCNTL_CKSM_FILE); + ADD_INT(SQLITE_FCNTL_RESET_CACHE); + #undef ADD_INT return 0; } From 3f9c1e27693482fe49619cc6860465769706a0e4 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sat, 4 Jan 2025 18:21:54 -0800 Subject: [PATCH 02/13] sqlite3_file_control --- Modules/_sqlite/module.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index c9db22e2228139..9fa55f6a7c741f 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -530,7 +530,10 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_FCNTL_PRAGMA); ADD_INT(SQLITE_FCNTL_BUSYHANDLER); ADD_INT(SQLITE_FCNTL_TEMPFILENAME); +#if SQLITE_VERSION_NUMBER >= 3008000 ADD_INT(SQLITE_FCNTL_MMAP_SIZE); +#endif +#if SQLITE_VERSION_NUMBER >= 3009000 ADD_INT(SQLITE_FCNTL_TRACE); ADD_INT(SQLITE_FCNTL_HAS_MOVED); ADD_INT(SQLITE_FCNTL_SYNC); @@ -539,22 +542,50 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_FCNTL_WAL_BLOCK); ADD_INT(SQLITE_FCNTL_ZIPVFS); ADD_INT(SQLITE_FCNTL_RBU); +#endif +#if SQLITE_VERSION_NUMBER >= 3010000 ADD_INT(SQLITE_FCNTL_VFS_POINTER); +#endif +#if SQLITE_VERSION_NUMBER >= 3011000 ADD_INT(SQLITE_FCNTL_JOURNAL_POINTER); +#endif +#if SQLITE_VERSION_NUMBER >= 3015000 ADD_INT(SQLITE_FCNTL_WIN32_GET_HANDLE); ADD_INT(SQLITE_FCNTL_PDB); +#endif +#if SQLITE_VERSION_NUMBER >= 3021000 ADD_INT(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE); ADD_INT(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE); ADD_INT(SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE); +#endif +#if SQLITE_VERSION_NUMBER >= 3023000 ADD_INT(SQLITE_FCNTL_LOCK_TIMEOUT); +#endif +#if SQLITE_VERSION_NUMBER >= 3025000 ADD_INT(SQLITE_FCNTL_DATA_VERSION); +#endif +#if SQLITE_VERSION_NUMBER >= 3028000 ADD_INT(SQLITE_FCNTL_SIZE_LIMIT); +#endif +#if SQLITE_VERSION_NUMBER >= 3031000 ADD_INT(SQLITE_FCNTL_CKPT_DONE); +#endif +#if SQLITE_VERSION_NUMBER >= 3032000 ADD_INT(SQLITE_FCNTL_RESERVE_BYTES); ADD_INT(SQLITE_FCNTL_CKPT_START); +#endif +#if SQLITE_VERSION_NUMBER >= 3035000 ADD_INT(SQLITE_FCNTL_EXTERNAL_READER); +#endif +#if SQLITE_VERSION_NUMBER >= 3036000 ADD_INT(SQLITE_FCNTL_CKSM_FILE); +#endif +#if SQLITE_VERSION_NUMBER >= 3040000 ADD_INT(SQLITE_FCNTL_RESET_CACHE); +#endif +#if SQLITE_VERSION_NUMBER >= 3048000 + ADD_INT(SQLITE_FCNTL_NULL_IO); +#endif #undef ADD_INT return 0; From f9eb73d8cb18ad76db91dfdb0d35b74ac71d5ecf Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sat, 4 Jan 2025 18:22:45 -0800 Subject: [PATCH 03/13] sqlite3_file_control --- Modules/_sqlite/module.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 9fa55f6a7c741f..7a68b07b6d69a0 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -530,10 +530,7 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_FCNTL_PRAGMA); ADD_INT(SQLITE_FCNTL_BUSYHANDLER); ADD_INT(SQLITE_FCNTL_TEMPFILENAME); -#if SQLITE_VERSION_NUMBER >= 3008000 ADD_INT(SQLITE_FCNTL_MMAP_SIZE); -#endif -#if SQLITE_VERSION_NUMBER >= 3009000 ADD_INT(SQLITE_FCNTL_TRACE); ADD_INT(SQLITE_FCNTL_HAS_MOVED); ADD_INT(SQLITE_FCNTL_SYNC); @@ -542,17 +539,10 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_FCNTL_WAL_BLOCK); ADD_INT(SQLITE_FCNTL_ZIPVFS); ADD_INT(SQLITE_FCNTL_RBU); -#endif -#if SQLITE_VERSION_NUMBER >= 3010000 ADD_INT(SQLITE_FCNTL_VFS_POINTER); -#endif -#if SQLITE_VERSION_NUMBER >= 3011000 ADD_INT(SQLITE_FCNTL_JOURNAL_POINTER); -#endif -#if SQLITE_VERSION_NUMBER >= 3015000 ADD_INT(SQLITE_FCNTL_WIN32_GET_HANDLE); ADD_INT(SQLITE_FCNTL_PDB); -#endif #if SQLITE_VERSION_NUMBER >= 3021000 ADD_INT(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE); ADD_INT(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE); From dc600555d1986a1233ae06827ffabcbf1a304551 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sun, 5 Jan 2025 07:17:49 -0800 Subject: [PATCH 04/13] sqlite3_file_control --- Lib/test/test_sqlite3/test_dbapi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 7845cdf1b23b32..2bbc9feda16d96 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -756,7 +756,7 @@ def test_wal_preservation(self): def test_file_control_raises(self): - with contextlib.closing(sqlite.connect(database=":memory:")) as cx: + with memory_database() as con: with self.assertRaises(sqlite.ProgrammingError): cx.set_file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) @@ -1888,8 +1888,6 @@ def test_on_conflict_replace(self): self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')]) - - @requires_subprocess() class MultiprocessTests(unittest.TestCase): CONNECTION_TIMEOUT = 0 # Disable the busy timeout. From 4867672026a1c45aceb5e11327840e9666b3896d Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sun, 5 Jan 2025 14:26:49 -0800 Subject: [PATCH 05/13] sqlite3_file_control --- Lib/test/test_sqlite3/test_dbapi.py | 7 +- ...-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst | 4 +- Modules/_sqlite/clinic/connection.c.h | 44 ++++----- Modules/_sqlite/connection.c | 97 ++++++++++++++++--- Modules/_sqlite/module.c | 3 +- 5 files changed, 117 insertions(+), 38 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 2bbc9feda16d96..5d4ae70d7309f9 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -736,11 +736,12 @@ def test_database_keyword(self): with contextlib.closing(sqlite.connect(database=":memory:")) as cx: self.assertEqual(type(cx), sqlite.Connection) + @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") def test_wal_preservation(self): with tempfile.TemporaryDirectory() as dirname: path = os.path.join(dirname, "db.sqlite") with contextlib.closing(sqlite.connect(path)) as cx: - cx.set_file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) cu = cx.cursor() cu.execute("PRAGMA journal_mode = WAL") cu.execute("CREATE TABLE foo (id int)") @@ -756,9 +757,9 @@ def test_wal_preservation(self): def test_file_control_raises(self): - with memory_database() as con: + with memory_database() as cx: with self.assertRaises(sqlite.ProgrammingError): - cx.set_file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) class CursorTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst index cb700765fd797a..303e42d203b3ce 100644 --- a/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst +++ b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst @@ -1 +1,3 @@ -sqlite Connection objects now expose a method set_file_control, which is a thin wrapper for `sqlite3_file_control https://www.sqlite.org/c3ref/file_control.html`_. +sqlite Connection objects now expose a method +:meth:`~sqlite3.Connection.file_control`, which is a thin wrapper for +`sqlite3_file_control `_. diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 9d96e1bbd93f29..b687c3f1de8a93 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1455,28 +1455,28 @@ pysqlite_connection_create_collation(pysqlite_Connection *self, PyTypeObject *cl return return_value; } -PyDoc_STRVAR(pysqlite_connection_set_file_control__doc__, -"set_file_control($self, op, arg, /, dbname=)\n" +PyDoc_STRVAR(pysqlite_connection_file_control__doc__, +"file_control($self, op, arg, /, name=\'main\')\n" "--\n" "\n" "Invoke a file control method on the database.\n" "\n" " op\n" -" a SQLITE_FCNTL_ constant\n" +" The SQLITE_FCNTL_* constant to invoke.\n" " arg\n" -" argument to pass\n" -" dbname\n" -" database name"); +" The argument to pass to the operation.\n" +" name\n" +" The database name to operate against."); -#define PYSQLITE_CONNECTION_SET_FILE_CONTROL_METHODDEF \ - {"set_file_control", _PyCFunction_CAST(pysqlite_connection_set_file_control), METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_set_file_control__doc__}, +#define PYSQLITE_CONNECTION_FILE_CONTROL_METHODDEF \ + {"file_control", _PyCFunction_CAST(pysqlite_connection_file_control), METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_file_control__doc__}, static PyObject * -pysqlite_connection_set_file_control_impl(pysqlite_Connection *self, int op, - long arg, const char *dbname); +pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, + long arg, const char *name); static PyObject * -pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +pysqlite_connection_file_control(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) @@ -1488,7 +1488,7 @@ pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const PyObject *ob_item[NUM_KEYWORDS]; } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_item = { &_Py_ID(dbname), }, + .ob_item = { &_Py_ID(name), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1497,10 +1497,10 @@ pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"", "", "dbname", NULL}; + static const char * const _keywords[] = {"", "", "name", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, - .fname = "set_file_control", + .fname = "file_control", .kwtuple = KWTUPLE, }; #undef KWTUPLE @@ -1508,7 +1508,7 @@ pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; int op; long arg; - const char *dbname = NULL; + const char *name = "main"; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 2, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -1527,20 +1527,20 @@ pysqlite_connection_set_file_control(pysqlite_Connection *self, PyObject *const goto skip_optional_pos; } if (!PyUnicode_Check(args[2])) { - _PyArg_BadArgument("set_file_control", "argument 'dbname'", "str", args[2]); + _PyArg_BadArgument("file_control", "argument 'name'", "str", args[2]); goto exit; } - Py_ssize_t dbname_length; - dbname = PyUnicode_AsUTF8AndSize(args[2], &dbname_length); - if (dbname == NULL) { + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(args[2], &name_length); + if (name == NULL) { goto exit; } - if (strlen(dbname) != (size_t)dbname_length) { + if (strlen(name) != (size_t)name_length) { PyErr_SetString(PyExc_ValueError, "embedded null character"); goto exit; } skip_optional_pos: - return_value = pysqlite_connection_set_file_control_impl(self, op, arg, dbname); + return_value = pysqlite_connection_file_control_impl(self, op, arg, name); exit: return return_value; @@ -1972,4 +1972,4 @@ getconfig(pysqlite_Connection *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=3465ff17f7ac6104 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=bbacc8ab05169bc5 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index d316746633725d..8f2572284186a3 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2173,34 +2173,109 @@ pysqlite_connection_create_collation_impl(pysqlite_Connection *self, Py_RETURN_NONE; } +static inline bool +is_int_fcntl(const int op) +{ + switch (op) { + case SQLITE_FCNTL_LOCKSTATE: + case SQLITE_FCNTL_GET_LOCKPROXYFILE: + case SQLITE_FCNTL_SET_LOCKPROXYFILE: + case SQLITE_FCNTL_LAST_ERRNO: + case SQLITE_FCNTL_SIZE_HINT: + case SQLITE_FCNTL_CHUNK_SIZE: + case SQLITE_FCNTL_FILE_POINTER: + case SQLITE_FCNTL_SYNC_OMITTED: + case SQLITE_FCNTL_WIN32_AV_RETRY: + case SQLITE_FCNTL_PERSIST_WAL: + case SQLITE_FCNTL_OVERWRITE: + case SQLITE_FCNTL_POWERSAFE_OVERWRITE: + case SQLITE_FCNTL_PRAGMA: + case SQLITE_FCNTL_BUSYHANDLER: + case SQLITE_FCNTL_MMAP_SIZE: + case SQLITE_FCNTL_TRACE: + case SQLITE_FCNTL_HAS_MOVED: + case SQLITE_FCNTL_SYNC: + case SQLITE_FCNTL_COMMIT_PHASETWO: + case SQLITE_FCNTL_WIN32_SET_HANDLE: + case SQLITE_FCNTL_WAL_BLOCK: + case SQLITE_FCNTL_ZIPVFS: + case SQLITE_FCNTL_RBU: + case SQLITE_FCNTL_VFS_POINTER: + case SQLITE_FCNTL_JOURNAL_POINTER: + case SQLITE_FCNTL_WIN32_GET_HANDLE: + case SQLITE_FCNTL_PDB: +#if SQLITE_VERSION_NUMBER >= 3021000 + case SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: + case SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: + case SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: +#endif +#if SQLITE_VERSION_NUMBER >= 3023000 + case SQLITE_FCNTL_LOCK_TIMEOUT: +#endif +#if SQLITE_VERSION_NUMBER >= 3025000 + case SQLITE_FCNTL_DATA_VERSION: +#endif +#if SQLITE_VERSION_NUMBER >= 3028000 + case SQLITE_FCNTL_SIZE_LIMIT: +#endif +#if SQLITE_VERSION_NUMBER >= 3031000 + case SQLITE_FCNTL_CKPT_DONE: +#endif +#if SQLITE_VERSION_NUMBER >= 3032000 + case SQLITE_FCNTL_RESERVE_BYTES: + case SQLITE_FCNTL_CKPT_START: +#endif +#if SQLITE_VERSION_NUMBER >= 3035000 + case SQLITE_FCNTL_EXTERNAL_READER: +#endif +#if SQLITE_VERSION_NUMBER >= 3036000 + case SQLITE_FCNTL_CKSM_FILE: +#endif +#if SQLITE_VERSION_NUMBER >= 3040000 + case SQLITE_FCNTL_RESET_CACHE: +#endif +#if SQLITE_VERSION_NUMBER >= 3048000 + case SQLITE_FCNTL_NULL_IO: +#endif + return true; + default: + return false; + } +} + /*[clinic input] -_sqlite3.Connection.set_file_control as pysqlite_connection_set_file_control +_sqlite3.Connection.file_control as pysqlite_connection_file_control op: int - a SQLITE_FCNTL_ constant + The SQLITE_FCNTL_* constant to invoke. arg: long - argument to pass + The argument to pass to the operation. / - dbname: str = NULL - database name + name: str = "main" + The database name to operate against. Invoke a file control method on the database. [clinic start generated code]*/ static PyObject * -pysqlite_connection_set_file_control_impl(pysqlite_Connection *self, int op, - long arg, const char *dbname) -/*[clinic end generated code: output=d9d2d311892893b6 input=0253798d9514fea2]*/ +pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, + long arg, const char *name) +/*[clinic end generated code: output=ab3230aaca500391 input=506d31506027e9ce]*/ { + if(!is_int_fcntl(op)) { + PyErr_Format(PyExc_ValueError, "unknown file control 'op': %d", op); + return NULL; + } + + int val = arg; int rc; - long val = arg; if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { return NULL; } Py_BEGIN_ALLOW_THREADS - rc = sqlite3_file_control(self->db, dbname, op, &val); + rc = sqlite3_file_control(self->db, name, op, &val); Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { @@ -2640,7 +2715,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_SET_AUTHORIZER_METHODDEF PYSQLITE_CONNECTION_SET_PROGRESS_HANDLER_METHODDEF PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF - PYSQLITE_CONNECTION_SET_FILE_CONTROL_METHODDEF + PYSQLITE_CONNECTION_FILE_CONTROL_METHODDEF SETLIMIT_METHODDEF GETLIMIT_METHODDEF SERIALIZE_METHODDEF diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 7a68b07b6d69a0..28645d6d2ae127 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -576,7 +576,8 @@ add_integer_constants(PyObject *module) { #if SQLITE_VERSION_NUMBER >= 3048000 ADD_INT(SQLITE_FCNTL_NULL_IO); #endif - +// When updating this list, also update PYSQLITE_LAST_VALID_FCNTL in module.h +// and is_int_fcntl in connection.c #undef ADD_INT return 0; } From 4bcb82c4415c4d4e8f134215ee0645949d68ae86 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sun, 5 Jan 2025 16:01:45 -0800 Subject: [PATCH 06/13] sqlite3_file_control --- .../next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst index 303e42d203b3ce..42dc829a444618 100644 --- a/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst +++ b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst @@ -1,3 +1,3 @@ sqlite Connection objects now expose a method -:meth:`~sqlite3.Connection.file_control`, which is a thin wrapper for +:meth:`sqlite3.Connection.file_control`, which is a thin wrapper for `sqlite3_file_control `_. From c42bbd5d3a28c4107bbb4a3012b31b027fe32a26 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Mon, 6 Jan 2025 22:45:03 -0800 Subject: [PATCH 07/13] change arg --- Modules/_sqlite/clinic/connection.c.h | 8 ++++---- Modules/_sqlite/connection.c | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index b687c3f1de8a93..073b5dd34882df 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1473,7 +1473,7 @@ PyDoc_STRVAR(pysqlite_connection_file_control__doc__, static PyObject * pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, - long arg, const char *name); + int arg, const char *name); static PyObject * pysqlite_connection_file_control(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -1507,7 +1507,7 @@ pysqlite_connection_file_control(pysqlite_Connection *self, PyObject *const *arg PyObject *argsbuf[3]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; int op; - long arg; + int arg; const char *name = "main"; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, @@ -1519,7 +1519,7 @@ pysqlite_connection_file_control(pysqlite_Connection *self, PyObject *const *arg if (op == -1 && PyErr_Occurred()) { goto exit; } - arg = PyLong_AsLong(args[1]); + arg = PyLong_AsInt(args[1]); if (arg == -1 && PyErr_Occurred()) { goto exit; } @@ -1972,4 +1972,4 @@ getconfig(pysqlite_Connection *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=bbacc8ab05169bc5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a1064802823d5bed input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 8f2572284186a3..914d22d9fca022 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2248,7 +2248,7 @@ _sqlite3.Connection.file_control as pysqlite_connection_file_control op: int The SQLITE_FCNTL_* constant to invoke. - arg: long + arg: int The argument to pass to the operation. / name: str = "main" @@ -2259,8 +2259,8 @@ Invoke a file control method on the database. static PyObject * pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, - long arg, const char *name) -/*[clinic end generated code: output=ab3230aaca500391 input=506d31506027e9ce]*/ + int arg, const char *name) +/*[clinic end generated code: output=8a9f04093fc1f59c input=3833150b82ba5488]*/ { if(!is_int_fcntl(op)) { PyErr_Format(PyExc_ValueError, "unknown file control 'op': %d", op); From 466401ccd713f4525982d9829f1b04fbb27005fb Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Mon, 6 Jan 2025 23:03:48 -0800 Subject: [PATCH 08/13] remove unsupported opcodes --- Modules/_sqlite/module.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 28645d6d2ae127..9bfd1e55787ac5 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -525,11 +525,9 @@ add_integer_constants(PyObject *module) { ADD_INT(SQLITE_FCNTL_WIN32_AV_RETRY); ADD_INT(SQLITE_FCNTL_PERSIST_WAL); ADD_INT(SQLITE_FCNTL_OVERWRITE); - ADD_INT(SQLITE_FCNTL_VFSNAME); ADD_INT(SQLITE_FCNTL_POWERSAFE_OVERWRITE); ADD_INT(SQLITE_FCNTL_PRAGMA); ADD_INT(SQLITE_FCNTL_BUSYHANDLER); - ADD_INT(SQLITE_FCNTL_TEMPFILENAME); ADD_INT(SQLITE_FCNTL_MMAP_SIZE); ADD_INT(SQLITE_FCNTL_TRACE); ADD_INT(SQLITE_FCNTL_HAS_MOVED); From cc3bed932af332c7946d98497409618e84220135 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Mon, 6 Jan 2025 23:07:35 -0800 Subject: [PATCH 09/13] update docstring --- Modules/_sqlite/clinic/connection.c.h | 6 ++++-- Modules/_sqlite/connection.c | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 073b5dd34882df..12a1e9e901dc47 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1466,7 +1466,9 @@ PyDoc_STRVAR(pysqlite_connection_file_control__doc__, " arg\n" " The argument to pass to the operation.\n" " name\n" -" The database name to operate against."); +" The database name to operate against.\n" +"\n" +"Opcodes which take non-integer arguments are not supported."); #define PYSQLITE_CONNECTION_FILE_CONTROL_METHODDEF \ {"file_control", _PyCFunction_CAST(pysqlite_connection_file_control), METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_file_control__doc__}, @@ -1972,4 +1974,4 @@ getconfig(pysqlite_Connection *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=a1064802823d5bed input=a9049054013a1b77]*/ +/*[clinic end generated code: output=bec78c3d082dfc46 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 914d22d9fca022..db28712f97f3e6 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2255,12 +2255,14 @@ _sqlite3.Connection.file_control as pysqlite_connection_file_control The database name to operate against. Invoke a file control method on the database. + +Opcodes which take non-integer arguments are not supported. [clinic start generated code]*/ static PyObject * pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, int arg, const char *name) -/*[clinic end generated code: output=8a9f04093fc1f59c input=3833150b82ba5488]*/ +/*[clinic end generated code: output=8a9f04093fc1f59c input=8819ab1022e6a5ee]*/ { if(!is_int_fcntl(op)) { PyErr_Format(PyExc_ValueError, "unknown file control 'op': %d", op); From 773dc5f5e9aa4b2581e0640a7b42ce1437205921 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Mon, 6 Jan 2025 23:11:21 -0800 Subject: [PATCH 10/13] add reference docs --- Doc/library/sqlite3.rst | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 096892b605b99c..55da9f58c79248 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -579,6 +579,61 @@ Module constants .. deprecated-removed:: 3.12 3.14 The :data:`!version` and :data:`!version_info` constants. +.. _sqlite3-fcntl-constants: + +.. data:: SQLITE_FCNTL_LOCKSTATE + SQLITE_FCNTL_GET_LOCKPROXYFILE + SQLITE_FCNTL_SET_LOCKPROXYFILE + SQLITE_FCNTL_LAST_ERRNO + SQLITE_FCNTL_SIZE_HINT + SQLITE_FCNTL_CHUNK_SIZE + SQLITE_FCNTL_FILE_POINTER + SQLITE_FCNTL_SYNC_OMITTED + SQLITE_FCNTL_WIN32_AV_RETRY + SQLITE_FCNTL_PERSIST_WAL + SQLITE_FCNTL_OVERWRITE + SQLITE_FCNTL_POWERSAFE_OVERWRITE + SQLITE_FCNTL_PRAGMA + SQLITE_FCNTL_BUSYHANDLER + SQLITE_FCNTL_MMAP_SIZE + SQLITE_FCNTL_TRACE + SQLITE_FCNTL_HAS_MOVED + SQLITE_FCNTL_SYNC + SQLITE_FCNTL_COMMIT_PHASETWO + SQLITE_FCNTL_WIN32_SET_HANDLE + SQLITE_FCNTL_WAL_BLOCK + SQLITE_FCNTL_ZIPVFS + SQLITE_FCNTL_RBU + SQLITE_FCNTL_VFS_POINTER + SQLITE_FCNTL_JOURNAL_POINTER + SQLITE_FCNTL_WIN32_GET_HANDLE + SQLITE_FCNTL_PDB + SQLITE_FCNTL_BEGIN_ATOMIC_WRITE + SQLITE_FCNTL_COMMIT_ATOMIC_WRITE + SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE + SQLITE_FCNTL_LOCK_TIMEOUT + SQLITE_FCNTL_DATA_VERSION + SQLITE_FCNTL_SIZE_LIMIT + SQLITE_FCNTL_CKPT_DONE + SQLITE_FCNTL_RESERVE_BYTES + SQLITE_FCNTL_CKPT_START + SQLITE_FCNTL_EXTERNAL_READER + SQLITE_FCNTL_CKSM_FILE + SQLITE_FCNTL_RESET_CACHE + SQLITE_FCNTL_NULL_IO + + These constants are used for the :meth:`Connection.file_control` method. + + The availability of these constants varies depending on the version of SQLite + Python was compiled with. + + .. versionadded:: 3.14 + + .. seealso:: + + https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html + SQLite docs: Standard File Control Opcodes + .. _sqlite3-connection-objects: Connection objects @@ -1288,6 +1343,24 @@ Connection objects .. versionadded:: 3.12 + .. method:: file_control(op, val, /, name="main") + + Invoke a file control method on the database. + Opcodes which take non-integer arguments are not supported. + + :param int op: + The :ref:`SQLITE_FCNTL_* constant ` to invoke. + + :param int arg: + The argument to pass to the operation. + + :param str name: + the database name to operate against. + + :rtype: int + + .. versionadded:: 3.14 + .. method:: serialize(*, name="main") Serialize a database into a :class:`bytes` object. For an From bd2a2a9d8bfe727905fefa7054064ca2187f5764 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 10 Feb 2025 01:49:54 +0100 Subject: [PATCH 11/13] gh-129928: Rework sqlite3 error helpers Add a private API for raising DB-API compatible exceptions based on the result code of SQLite C APIs. Some APIs do not store the error indicator on the database pointer, so we need to be able to deduce the DB-API compatible exception directly from the error code. - rename _pysqlite_seterror() as set_error_from_db() - introduce set_error_from_code() --- Modules/_sqlite/blob.c | 2 +- Modules/_sqlite/connection.c | 32 +++++++++++++++++++------------- Modules/_sqlite/cursor.c | 13 ++++++------- Modules/_sqlite/statement.c | 2 +- Modules/_sqlite/util.c | 24 ++++++++++++++++++------ Modules/_sqlite/util.h | 4 ++-- 6 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 390375628bfb4f..35d090e3ca2dce 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -119,7 +119,7 @@ static void blob_seterror(pysqlite_Blob *self, int rc) { assert(self->connection != NULL); - _pysqlite_seterror(self->connection->state, self->connection->db); + set_error_from_db(self->connection->state, self->connection->db); } static PyObject * diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 16afd7eada113f..f28327daf96073 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -188,7 +188,7 @@ connection_exec_stmt(pysqlite_Connection *self, const char *sql) Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - (void)_pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return -1; } return 0; @@ -274,7 +274,7 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, PyObject *database, pysqlite_state *state = pysqlite_get_state_by_type(Py_TYPE(self)); if (rc != SQLITE_OK) { - _pysqlite_seterror(state, db); + set_error_from_db(state, db); goto error; } @@ -607,11 +607,11 @@ blobopen_impl(pysqlite_Connection *self, const char *table, const char *col, Py_END_ALLOW_THREADS if (rc == SQLITE_MISUSE) { - PyErr_Format(self->state->InterfaceError, sqlite3_errstr(rc)); + set_error_from_code(self->state, rc); return NULL; } else if (rc != SQLITE_OK) { - _pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return NULL; } @@ -1307,6 +1307,12 @@ create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, "SQLite 3.25.0 or higher"); return NULL; } + int limit = sqlite3_limit(self->db, SQLITE_LIMIT_FUNCTION_ARG, -1); + if (num_params < -1 || num_params > limit) { + return PyErr_Format(self->ProgrammingError, + "'num_params' must be between -1 and %d, not %d", + limit, num_params); + } if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { return NULL; @@ -1333,9 +1339,9 @@ create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, } if (rc != SQLITE_OK) { - // Errors are not set on the database connection, so we cannot - // use _pysqlite_seterror(). - PyErr_SetString(self->ProgrammingError, sqlite3_errstr(rc)); + /* Errors are not set on the database connection; use result code + * instead. */ + set_error_from_code(self->state, rc); return NULL; } Py_RETURN_NONE; @@ -2090,7 +2096,7 @@ pysqlite_connection_backup_impl(pysqlite_Connection *self, Py_END_ALLOW_THREADS if (bck_handle == NULL) { - _pysqlite_seterror(self->state, bck_conn); + set_error_from_db(self->state, bck_conn); return NULL; } @@ -2128,7 +2134,7 @@ pysqlite_connection_backup_impl(pysqlite_Connection *self, Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - _pysqlite_seterror(self->state, bck_conn); + set_error_from_db(self->state, bck_conn); return NULL; } @@ -2186,7 +2192,7 @@ pysqlite_connection_create_collation_impl(pysqlite_Connection *self, if (callable != Py_None) { free_callback_context(ctx); } - _pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return NULL; } @@ -2304,7 +2310,7 @@ deserialize_impl(pysqlite_Connection *self, Py_buffer *data, Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - (void)_pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return NULL; } Py_RETURN_NONE; @@ -2499,7 +2505,7 @@ setconfig_impl(pysqlite_Connection *self, int op, int enable) int actual; int rc = sqlite3_db_config(self->db, op, enable, &actual); if (rc != SQLITE_OK) { - (void)_pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return NULL; } if (enable != actual) { @@ -2534,7 +2540,7 @@ getconfig_impl(pysqlite_Connection *self, int op) int current; int rc = sqlite3_db_config(self->db, op, -1, ¤t); if (rc != SQLITE_OK) { - (void)_pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return -1; } return current; diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 02d598040775b0..ad3587d88dd854 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -505,7 +505,7 @@ begin_transaction(pysqlite_Connection *self) Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - (void)_pysqlite_seterror(self->state, self->db); + set_error_from_db(self->state, self->db); return -1; } @@ -715,7 +715,7 @@ bind_parameters(pysqlite_state *state, pysqlite_Statement *self, if (rc != SQLITE_OK) { PyObject *exc = PyErr_GetRaisedException(); sqlite3 *db = sqlite3_db_handle(self->st); - _pysqlite_seterror(state, db); + set_error_from_db(state, db); _PyErr_ChainExceptions1(exc); return; } @@ -764,7 +764,7 @@ bind_parameters(pysqlite_state *state, pysqlite_Statement *self, if (rc != SQLITE_OK) { PyObject *exc = PyErr_GetRaisedException(); sqlite3 *db = sqlite3_db_handle(self->st); - _pysqlite_seterror(state, db); + set_error_from_db(state, db); _PyErr_ChainExceptions1(exc); return; } @@ -896,7 +896,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation PyErr_Clear(); } } - _pysqlite_seterror(state, self->connection->db); + set_error_from_db(state, self->connection->db); goto error; } @@ -1087,7 +1087,7 @@ pysqlite_cursor_executescript_impl(pysqlite_Cursor *self, return Py_NewRef((PyObject *)self); error: - _pysqlite_seterror(self->connection->state, db); + set_error_from_db(self->connection->state, db); return NULL; } @@ -1122,8 +1122,7 @@ pysqlite_cursor_iternext(PyObject *op) Py_CLEAR(self->statement); } else if (rc != SQLITE_ROW) { - (void)_pysqlite_seterror(self->connection->state, - self->connection->db); + set_error_from_db(self->connection->state, self->connection->db); (void)stmt_reset(self->statement); Py_CLEAR(self->statement); Py_DECREF(row); diff --git a/Modules/_sqlite/statement.c b/Modules/_sqlite/statement.c index facced0dfbfafd..736e60fd778287 100644 --- a/Modules/_sqlite/statement.c +++ b/Modules/_sqlite/statement.c @@ -62,7 +62,7 @@ pysqlite_statement_create(pysqlite_Connection *connection, PyObject *sql) Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - _pysqlite_seterror(state, db); + set_error_from_db(state, db); return NULL; } diff --git a/Modules/_sqlite/util.c b/Modules/_sqlite/util.c index b0622e66928f47..103248ff55aa0c 100644 --- a/Modules/_sqlite/util.c +++ b/Modules/_sqlite/util.c @@ -118,18 +118,31 @@ raise_exception(PyObject *type, int errcode, const char *errmsg) Py_XDECREF(exc); } +void +set_error_from_code(pysqlite_state *state, int code) +{ + PyObject *exc_class = get_exception_class(state, code); + if (exc_class == NULL) { + // No new exception need be raised. + return; + } + + const char *errmsg = sqlite3_errstr(code); + assert(errmsg != NULL); + raise_exception(exc_class, code, errmsg); +} + /** * 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) +void +set_error_from_db(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; + // No new exception need be raised. + return; } /* Create and set the exception. */ @@ -137,7 +150,6 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db) // sqlite3_errmsg() always returns an UTF-8 encoded message const char *errmsg = sqlite3_errmsg(db); raise_exception(exc_class, extended_errcode, errmsg); - return extended_errcode; } #ifdef WORDS_BIGENDIAN diff --git a/Modules/_sqlite/util.h b/Modules/_sqlite/util.h index 68b1a8cb67ace3..f8e45baffaefe3 100644 --- a/Modules/_sqlite/util.h +++ b/Modules/_sqlite/util.h @@ -30,9 +30,9 @@ /** * 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); +void set_error_from_db(pysqlite_state *state, sqlite3 *db); +void set_error_from_code(pysqlite_state *state, int code); sqlite_int64 _pysqlite_long_as_int64(PyObject * value); From b17f832a1c3683dba799a46d39c33832a84f65b7 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sun, 9 Feb 2025 18:10:39 -0800 Subject: [PATCH 12/13] use set_error_from_code --- Modules/_sqlite/connection.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 4a05a5705f5947..16d61b755bfc29 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2307,7 +2307,7 @@ pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { - PyErr_SetString(self->ProgrammingError, sqlite3_errstr(rc)); + set_error_from_code(self->state, rc); return NULL; } From 8bb3c5732f20654929ebd337520861674b18b391 Mon Sep 17 00:00:00 2001 From: Josh Snyder Date: Sun, 9 Feb 2025 19:24:18 -0800 Subject: [PATCH 13/13] fix test --- Lib/test/test_sqlite3/test_dbapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 85abb5bb9a66ba..0097d0b50c2119 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -747,7 +747,7 @@ def test_wal_preservation(self): def test_file_control_raises(self): with memory_database() as cx: - with self.assertRaises(sqlite.ProgrammingError): + with self.assertRaises(sqlite.InternalError): cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1)