Skip to content

gh-103489: Add get/set config methods to sqlite3.Connection #103506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 26, 2023
Merged
56 changes: 56 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,30 @@ def test_connection_bad_reinit(self):
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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 80 additions & 1 deletion Modules/_sqlite/clinic/connection.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 117 additions & 0 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include "prepare_protocol.h"
#include "util.h"

#include <stdbool.h>

#if SQLITE_VERSION_NUMBER >= 3014000
#define HAVE_TRACE_V2
#endif
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -2424,6 +2539,8 @@ static PyMethodDef connection_methods[] = {
DESERIALIZE_METHODDEF
CREATE_WINDOW_FUNCTION_METHODDEF
BLOBOPEN_METHODDEF
SETCONFIG_METHODDEF
GETCONFIG_METHODDEF
{NULL, NULL}
};

Expand Down
43 changes: 43 additions & 0 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down