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 diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 488b401fb0054d..5d4ae70d7309f9 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,31 @@ 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.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 memory_database() as cx: + with self.assertRaises(sqlite.ProgrammingError): + 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 42eb6eb2f12554..12a1e9e901dc47 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -1455,6 +1455,99 @@ pysqlite_connection_create_collation(pysqlite_Connection *self, PyTypeObject *cl 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(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(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(self, op, arg, name); + +exit: + return return_value; +} + #if defined(PY_SQLITE_HAVE_SERIALIZE) PyDoc_STRVAR(serialize__doc__, @@ -1881,4 +1974,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=bec78c3d082dfc46 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index fc03e4a085c179..db28712f97f3e6 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -2173,6 +2173,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) { + 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 +2717,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 698e81d9b897d0..9bfd1e55787ac5 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -514,6 +514,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; }