diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 484260e63dd5f2..ee5c1dbb1e6a41 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -588,6 +588,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 @@ -1290,6 +1345,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 diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 73b40e82a96811..1173f26c1022ea 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -26,6 +26,7 @@ import sqlite3 as sqlite import subprocess import sys +import tempfile import threading import unittest import urllib.parse @@ -727,6 +728,39 @@ def test_database_keyword(self): with contextlib.closing(sqlite.connect(database=":memory:")) as cx: self.assertEqual(type(cx), sqlite.Connection) + # @hashbrowncipher skipped this test on mac, don't know why, rerunning to test it + 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.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + # Check that it was set successfully: + rc = cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, -1) + assert rc == 1, f"cx.file_control(SQLITE_FCNTL_PERSIST_WAL) failed to set flag" + + cu = cx.cursor() + result = cu.execute("PRAGMA journal_mode = WAL").fetchall() + assert result == [('wal',)], f"journal_mode could not be set to WAL, is {result}" + 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: + # Check that we can read the default value when we didn't set it explicitly: + rc = cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, -1) + assert rc == 0, f"SQLITE_FCNTL_PERSIST_WAL should be 0 by default, not {rc}" + + 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 memory_database() as cx: + with self.assertRaises(sqlite.InternalError): + cx.file_control(sqlite.SQLITE_FCNTL_PERSIST_WAL, 1) + class CursorTests(unittest.TestCase): def setUp(self): 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..42dc829a444618 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-05-00-55-41.gh-issue-128505.Nf5FY2.rst @@ -0,0 +1,3 @@ +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 abb864eb030757..fff019d0ea05bb 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1292,6 +1292,101 @@ pysqlite_connection_create_collation(PyObject *self, PyTypeObject *cls, PyObject return return_value; } +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" +" The SQLITE_FCNTL_* constant to invoke.\n" +" arg\n" +" The argument to pass to the operation.\n" +" name\n" +" 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__}, + +static PyObject * +pysqlite_connection_file_control_impl(pysqlite_Connection *self, int op, + int arg, const char *name); + +static PyObject * +pysqlite_connection_file_control(PyObject *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 + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(name), }, + }; + #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[] = {"", "", "name", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "file_control", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + int op; + int arg; + const char *name = "main"; + + 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_AsInt(args[1]); + if (arg == -1 && PyErr_Occurred()) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + if (!PyUnicode_Check(args[2])) { + _PyArg_BadArgument("file_control", "argument 'name'", "str", args[2]); + goto exit; + } + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(args[2], &name_length); + if (name == NULL) { + goto exit; + } + if (strlen(name) != (size_t)name_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } +skip_optional_pos: + return_value = pysqlite_connection_file_control_impl((pysqlite_Connection *)self, op, arg, name); + +exit: + return return_value; +} + #if defined(PY_SQLITE_HAVE_SERIALIZE) PyDoc_STRVAR(serialize__doc__, @@ -1722,4 +1817,4 @@ getconfig(PyObject *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=16d44c1d8a45e622 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7065aa07d7223767 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index bd44ff31b87c67..5afef465caa796 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1313,9 +1313,18 @@ 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; } + if (check_num_params(self, num_params, "num_params") < 0) { return NULL; } @@ -2210,6 +2219,122 @@ 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.file_control as pysqlite_connection_file_control + + op: int + The SQLITE_FCNTL_* constant to invoke. + arg: int + The argument to pass to the operation. + / + name: str = "main" + 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=8819ab1022e6a5ee]*/ +{ + if(!is_int_fcntl(op)) { + PyErr_Format(PyExc_ValueError, "unknown file control 'op': %d", op); + return NULL; + } + + int val = arg; + int rc; + + if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_file_control(self->db, name, op, &val); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + set_error_from_code(self->state, rc); + return NULL; + } + + return PyLong_FromLong(val); +} + + #ifdef PY_SQLITE_HAVE_SERIALIZE /*[clinic input] @permit_long_docstring_body @@ -2644,6 +2769,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_SET_AUTHORIZER_METHODDEF PYSQLITE_CONNECTION_SET_PROGRESS_HANDLER_METHODDEF PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_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 512d9744d57416..a407390ee5edfe 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -537,6 +537,68 @@ 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_POWERSAFE_OVERWRITE); + ADD_INT(SQLITE_FCNTL_PRAGMA); + ADD_INT(SQLITE_FCNTL_BUSYHANDLER); + 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); +#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 +// 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; }