aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorErlend E. Aasland <erlend.aasland@protonmail.com>2023-04-26 21:57:48 +0200
committerGitHub <noreply@github.com>2023-04-26 19:57:48 +0000
commitbb8aa7a2b41ad7649d66909e5266fcee039e63ed (patch)
tree69b936d96b77a82a34968934abdb97632efa455c
parent222c63fc6b91f42e7cc53574615f4e9b7a33c28f (diff)
downloadcpython-bb8aa7a2b41ad7649d66909e5266fcee039e63ed.tar.gz
cpython-bb8aa7a2b41ad7649d66909e5266fcee039e63ed.zip
gh-103489: Add get/set config methods to sqlite3.Connection (#103506)
-rw-r--r--Doc/library/sqlite3.rst56
-rw-r--r--Doc/whatsnew/3.12.rst5
-rw-r--r--Lib/test/test_sqlite3/test_dbapi.py24
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-13-13-17-47.gh-issue-103489.ZSZgmu.rst4
-rw-r--r--Modules/_sqlite/clinic/connection.c.h81
-rw-r--r--Modules/_sqlite/connection.c117
-rw-r--r--Modules/_sqlite/module.c43
7 files changed, 329 insertions, 1 deletions
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index 34de0bfdb0a..d47bcbfef8c 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -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
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 9d7943046ec..8fc95c49315 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -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
---------
diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py
index 3013abfa730..1bb0e13e356 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -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
index 00000000000..264564d018c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-13-13-17-47.gh-issue-103489.ZSZgmu.rst
@@ -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.
diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h
index 090f5d85567..182754cca36 100644
--- a/Modules/_sqlite/clinic/connection.c.h
+++ b/Modules/_sqlite/clinic/connection.c.h
@@ -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]*/
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index e3a373d85ff..aec3aa8bbf4 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -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}
};
diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c
index 6db3d51fd20..9c42faa232c 100644
--- a/Modules/_sqlite/module.c
+++ b/Modules/_sqlite/module.c
@@ -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;
}