]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-103489: Add get/set config methods to sqlite3.Connection (#103506)
authorErlend E. Aasland <erlend.aasland@protonmail.com>
Wed, 26 Apr 2023 19:57:48 +0000 (21:57 +0200)
committerGitHub <noreply@github.com>
Wed, 26 Apr 2023 19:57:48 +0000 (19:57 +0000)
Doc/library/sqlite3.rst
Doc/whatsnew/3.12.rst
Lib/test/test_sqlite3/test_dbapi.py
Misc/NEWS.d/next/Library/2023-04-13-13-17-47.gh-issue-103489.ZSZgmu.rst [new file with mode: 0644]
Modules/_sqlite/clinic/connection.c.h
Modules/_sqlite/connection.c
Modules/_sqlite/module.c

index 34de0bfdb0a8d4a42f0ca746ec65a3073770e97f..d47bcbfef8c43fa3feab07e85c62053345740047 100644 (file)
@@ -573,6 +573,38 @@ Module constants
       package, a third-party library which used to upstream changes to
       :mod:`!sqlite3`. Today, it carries no meaning or practical value.
 
+.. _sqlite3-dbconfig-constants:
+
+.. data:: SQLITE_DBCONFIG_DEFENSIVE
+          SQLITE_DBCONFIG_DQS_DDL
+          SQLITE_DBCONFIG_DQS_DML
+          SQLITE_DBCONFIG_ENABLE_FKEY
+          SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
+          SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION
+          SQLITE_DBCONFIG_ENABLE_QPSG
+          SQLITE_DBCONFIG_ENABLE_TRIGGER
+          SQLITE_DBCONFIG_ENABLE_VIEW
+          SQLITE_DBCONFIG_LEGACY_ALTER_TABLE
+          SQLITE_DBCONFIG_LEGACY_FILE_FORMAT
+          SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE
+          SQLITE_DBCONFIG_RESET_DATABASE
+          SQLITE_DBCONFIG_TRIGGER_EQP
+          SQLITE_DBCONFIG_TRUSTED_SCHEMA
+          SQLITE_DBCONFIG_WRITABLE_SCHEMA
+
+   These constants are used for the :meth:`Connection.setconfig`
+   and :meth:`~Connection.getconfig` methods.
+
+   The availability of these constants varies depending on the version of SQLite
+   Python was compiled with.
+
+   .. versionadded:: 3.12
+
+   .. seealso::
+
+     https://www.sqlite.org/c3ref/c_dbconfig_defensive.html
+        SQLite docs: Database Connection Configuration Options
+
 
 .. _sqlite3-connection-objects:
 
@@ -1219,6 +1251,30 @@ Connection objects
    .. _SQLite limit category: https://www.sqlite.org/c3ref/c_limit_attached.html
 
 
+   .. method:: getconfig(op, /)
+
+      Query a boolean connection configuration option.
+
+      :param int op:
+         A :ref:`SQLITE_DBCONFIG code <sqlite3-dbconfig-constants>`.
+
+      :rtype: bool
+
+      .. versionadded:: 3.12
+
+   .. method:: setconfig(op, enable=True, /)
+
+      Set a boolean connection configuration option.
+
+      :param int op:
+         A :ref:`SQLITE_DBCONFIG code <sqlite3-dbconfig-constants>`.
+
+      :param bool enable:
+         ``True`` if the configuration option should be enabled (default);
+         ``False`` if it should be disabled.
+
+      .. versionadded:: 3.12
+
    .. method:: serialize(*, name="main")
 
       Serialize a database into a :class:`bytes` object.  For an
index 9d7943046eca588c49c0e5d815681bc3d38d1a64..8fc95c493152e174a245f74c6ded1de65491c572 100644 (file)
@@ -411,6 +411,11 @@ sqlite3
   for overriding the SQLite extension entry point.
   (Contributed by Erlend E. Aasland in :gh:`103015`.)
 
+* Add :meth:`~sqlite3.Connection.getconfig` and
+  :meth:`~sqlite3.Connection.setconfig` to :class:`~sqlite3.Connection`
+  to make configuration changes to a database connection.
+  (Contributed by Erlend E. Aasland in :gh:`103489`.)
+
 threading
 ---------
 
index 3013abfa730ed53c21685a16c8faa262a3f00891..1bb0e13e356e78ec724d3c06159c5ad611d8a81b 100644 (file)
@@ -577,6 +577,30 @@ class ConnectionTests(unittest.TestCase):
                                    cx.executemany, "insert into t values(?)",
                                    ((v,) for v in range(3)))
 
+    def test_connection_config(self):
+        op = sqlite.SQLITE_DBCONFIG_ENABLE_FKEY
+        with memory_database() as cx:
+            with self.assertRaisesRegex(ValueError, "unknown"):
+                cx.getconfig(-1)
+
+            # Toggle and verify.
+            old = cx.getconfig(op)
+            new = not old
+            cx.setconfig(op, new)
+            self.assertEqual(cx.getconfig(op), new)
+
+            cx.setconfig(op)  # defaults to True
+            self.assertTrue(cx.getconfig(op))
+
+            # Check that foreign key support was actually enabled.
+            with cx:
+                cx.executescript("""
+                    create table t(t integer primary key);
+                    create table u(u, foreign key(u) references t(t));
+                """)
+            with self.assertRaisesRegex(sqlite.IntegrityError, "constraint"):
+                cx.execute("insert into u values(0)")
+
 
 class UninitialisedConnectionTests(unittest.TestCase):
     def setUp(self):
diff --git a/Misc/NEWS.d/next/Library/2023-04-13-13-17-47.gh-issue-103489.ZSZgmu.rst b/Misc/NEWS.d/next/Library/2023-04-13-13-17-47.gh-issue-103489.ZSZgmu.rst
new file mode 100644 (file)
index 0000000..264564d
--- /dev/null
@@ -0,0 +1,4 @@
+Add :meth:`~sqlite3.Connection.getconfig` and
+:meth:`~sqlite3.Connection.setconfig` to :class:`~sqlite3.Connection` to
+make configuration changes to a database connection. Patch by Erlend E.
+Aasland.
index 090f5d8556733968eecd1cfa0e024ac53d2dafe8..182754cca36d61c0541c00e96598dd37aaf3be05 100644 (file)
@@ -1568,6 +1568,85 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(setconfig__doc__,
+"setconfig($self, op, enable=True, /)\n"
+"--\n"
+"\n"
+"Set a boolean connection configuration option.\n"
+"\n"
+"  op\n"
+"    The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.");
+
+#define SETCONFIG_METHODDEF    \
+    {"setconfig", _PyCFunction_CAST(setconfig), METH_FASTCALL, setconfig__doc__},
+
+static PyObject *
+setconfig_impl(pysqlite_Connection *self, int op, int enable);
+
+static PyObject *
+setconfig(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    int op;
+    int enable = 1;
+
+    if (!_PyArg_CheckPositional("setconfig", nargs, 1, 2)) {
+        goto exit;
+    }
+    op = _PyLong_AsInt(args[0]);
+    if (op == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    if (nargs < 2) {
+        goto skip_optional;
+    }
+    enable = PyObject_IsTrue(args[1]);
+    if (enable < 0) {
+        goto exit;
+    }
+skip_optional:
+    return_value = setconfig_impl(self, op, enable);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(getconfig__doc__,
+"getconfig($self, op, /)\n"
+"--\n"
+"\n"
+"Query a boolean connection configuration option.\n"
+"\n"
+"  op\n"
+"    The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.");
+
+#define GETCONFIG_METHODDEF    \
+    {"getconfig", (PyCFunction)getconfig, METH_O, getconfig__doc__},
+
+static int
+getconfig_impl(pysqlite_Connection *self, int op);
+
+static PyObject *
+getconfig(pysqlite_Connection *self, PyObject *arg)
+{
+    PyObject *return_value = NULL;
+    int op;
+    int _return_value;
+
+    op = _PyLong_AsInt(arg);
+    if (op == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    _return_value = getconfig_impl(self, op);
+    if ((_return_value == -1) && PyErr_Occurred()) {
+        goto exit;
+    }
+    return_value = PyBool_FromLong((long)_return_value);
+
+exit:
+    return return_value;
+}
+
 #ifndef CREATE_WINDOW_FUNCTION_METHODDEF
     #define CREATE_WINDOW_FUNCTION_METHODDEF
 #endif /* !defined(CREATE_WINDOW_FUNCTION_METHODDEF) */
@@ -1587,4 +1666,4 @@ exit:
 #ifndef DESERIALIZE_METHODDEF
     #define DESERIALIZE_METHODDEF
 #endif /* !defined(DESERIALIZE_METHODDEF) */
-/*[clinic end generated code: output=833f0e27cd3a4560 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=8b03149c115ee6da input=a9049054013a1b77]*/
index e3a373d85ff9fef96dcd9936fb6530f4a1064aba..aec3aa8bbf4ed88c4fa6f0ff62e06f3d7e8539bd 100644 (file)
@@ -30,6 +30,8 @@
 #include "prepare_protocol.h"
 #include "util.h"
 
+#include <stdbool.h>
+
 #if SQLITE_VERSION_NUMBER >= 3014000
 #define HAVE_TRACE_V2
 #endif
@@ -2343,6 +2345,119 @@ getlimit_impl(pysqlite_Connection *self, int category)
     return setlimit_impl(self, category, -1);
 }
 
+static inline bool
+is_int_config(const int op)
+{
+    switch (op) {
+        case SQLITE_DBCONFIG_ENABLE_FKEY:
+        case SQLITE_DBCONFIG_ENABLE_TRIGGER:
+#if SQLITE_VERSION_NUMBER >= 3012002
+        case SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3013000
+        case SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3016000
+        case SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3020000
+        case SQLITE_DBCONFIG_ENABLE_QPSG:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3022000
+        case SQLITE_DBCONFIG_TRIGGER_EQP:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3024000
+        case SQLITE_DBCONFIG_RESET_DATABASE:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3026000
+        case SQLITE_DBCONFIG_DEFENSIVE:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3028000
+        case SQLITE_DBCONFIG_WRITABLE_SCHEMA:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3029000
+        case SQLITE_DBCONFIG_DQS_DDL:
+        case SQLITE_DBCONFIG_DQS_DML:
+        case SQLITE_DBCONFIG_LEGACY_ALTER_TABLE:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3030000
+        case SQLITE_DBCONFIG_ENABLE_VIEW:
+#endif
+#if SQLITE_VERSION_NUMBER >= 3031000
+        case SQLITE_DBCONFIG_LEGACY_FILE_FORMAT:
+        case SQLITE_DBCONFIG_TRUSTED_SCHEMA:
+#endif
+            return true;
+        default:
+            return false;
+    }
+}
+
+/*[clinic input]
+_sqlite3.Connection.setconfig as setconfig
+
+    op: int
+        The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.
+    enable: bool = True
+    /
+
+Set a boolean connection configuration option.
+[clinic start generated code]*/
+
+static PyObject *
+setconfig_impl(pysqlite_Connection *self, int op, int enable)
+/*[clinic end generated code: output=c60b13e618aff873 input=a10f1539c2d7da6b]*/
+{
+    if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
+        return NULL;
+    }
+    if (!is_int_config(op)) {
+        return PyErr_Format(PyExc_ValueError, "unknown config 'op': %d", op);
+    }
+
+    int actual;
+    int rc = sqlite3_db_config(self->db, op, enable, &actual);
+    if (rc != SQLITE_OK) {
+        (void)_pysqlite_seterror(self->state, self->db);
+        return NULL;
+    }
+    if (enable != actual) {
+        PyErr_SetString(self->state->OperationalError, "Unable to set config");
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+/*[clinic input]
+_sqlite3.Connection.getconfig as getconfig -> bool
+
+    op: int
+        The configuration verb; one of the sqlite3.SQLITE_DBCONFIG codes.
+    /
+
+Query a boolean connection configuration option.
+[clinic start generated code]*/
+
+static int
+getconfig_impl(pysqlite_Connection *self, int op)
+/*[clinic end generated code: output=25ac05044c7b78a3 input=b0526d7e432e3f2f]*/
+{
+    if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
+        return -1;
+    }
+    if (!is_int_config(op)) {
+        PyErr_Format(PyExc_ValueError, "unknown config 'op': %d", op);
+        return -1;
+    }
+
+    int current;
+    int rc = sqlite3_db_config(self->db, op, -1, &current);
+    if (rc != SQLITE_OK) {
+        (void)_pysqlite_seterror(self->state, self->db);
+        return -1;
+    }
+    return current;
+}
 
 static PyObject *
 get_autocommit(pysqlite_Connection *self, void *Py_UNUSED(ctx))
@@ -2424,6 +2539,8 @@ static PyMethodDef connection_methods[] = {
     DESERIALIZE_METHODDEF
     CREATE_WINDOW_FUNCTION_METHODDEF
     BLOBOPEN_METHODDEF
+    SETCONFIG_METHODDEF
+    GETCONFIG_METHODDEF
     {NULL, NULL}
 };
 
index 6db3d51fd202200c5a6f181bc8bd05c1c9bf23a2..9c42faa232c70d9f4eb021147af5a6ded8fcc58a 100644 (file)
@@ -499,6 +499,49 @@ add_integer_constants(PyObject *module) {
 #if SQLITE_VERSION_NUMBER >= 3008007
     ADD_INT(SQLITE_LIMIT_WORKER_THREADS);
 #endif
+
+    /*
+     * Database connection configuration options.
+     * See https://www.sqlite.org/c3ref/c_dbconfig_defensive.html
+     */
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_FKEY);
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_TRIGGER);
+#if SQLITE_VERSION_NUMBER >= 3012002
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3013000
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3016000
+    ADD_INT(SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3020000
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_QPSG);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3022000
+    ADD_INT(SQLITE_DBCONFIG_TRIGGER_EQP);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3024000
+    ADD_INT(SQLITE_DBCONFIG_RESET_DATABASE);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3026000
+    ADD_INT(SQLITE_DBCONFIG_DEFENSIVE);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3028000
+    ADD_INT(SQLITE_DBCONFIG_WRITABLE_SCHEMA);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3029000
+    ADD_INT(SQLITE_DBCONFIG_DQS_DDL);
+    ADD_INT(SQLITE_DBCONFIG_DQS_DML);
+    ADD_INT(SQLITE_DBCONFIG_LEGACY_ALTER_TABLE);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3030000
+    ADD_INT(SQLITE_DBCONFIG_ENABLE_VIEW);
+#endif
+#if SQLITE_VERSION_NUMBER >= 3031000
+    ADD_INT(SQLITE_DBCONFIG_LEGACY_FILE_FORMAT);
+    ADD_INT(SQLITE_DBCONFIG_TRUSTED_SCHEMA);
+#endif
 #undef ADD_INT
     return 0;
 }