[Python-checkins] bpo-45243: Add support for setting/getting `sqlite3` connection limits (GH-28463)

pablogsal webhook-mailer at python.org
Mon Nov 1 18:51:03 EDT 2021


https://github.com/python/cpython/commit/b6b38a82267ff70d2abaf2a8371327268887c97d
commit: b6b38a82267ff70d2abaf2a8371327268887c97d
branch: main
author: Erlend Egeberg Aasland <erlend.aasland at innova.no>
committer: pablogsal <Pablogsal at gmail.com>
date: 2021-11-01T22:50:53Z
summary:

bpo-45243: Add support for setting/getting `sqlite3` connection limits (GH-28463)

files:
A Misc/NEWS.d/next/Library/2021-09-20-01-25-09.bpo-45243.0pJf0U.rst
M Doc/library/sqlite3.rst
M Doc/whatsnew/3.11.rst
M Lib/test/test_sqlite3/test_dbapi.py
M Modules/_sqlite/clinic/connection.c.h
M Modules/_sqlite/connection.c
M Modules/_sqlite/module.c

diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index 19a4155542382..dcffc779cfba3 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -662,6 +662,40 @@ Connection Objects
       .. versionadded:: 3.7
 
 
+   .. method:: getlimit(category, /)
+
+      Get a connection run-time limit. *category* is the limit category to be
+      queried.
+
+      Example, query the maximum length of an SQL statement::
+
+         import sqlite3
+         con = sqlite3.connect(":memory:")
+         lim = con.getlimit(sqlite3.SQLITE_LIMIT_SQL_LENGTH)
+         print(f"SQLITE_LIMIT_SQL_LENGTH={lim}")
+
+      .. versionadded:: 3.11
+
+
+   .. method:: setlimit(category, limit, /)
+
+      Set a connection run-time limit. *category* is the limit category to be
+      set. *limit* is the new limit. If the new limit is a negative number, the
+      limit is unchanged.
+
+      Attempts to increase a limit above its hard upper bound are silently
+      truncated to the hard upper bound. Regardless of whether or not the limit
+      was changed, the prior value of the limit is returned.
+
+      Example, limit the number of attached databases to 1::
+
+         import sqlite3
+         con = sqlite3.connect(":memory:")
+         con.setlimit(sqlite3.SQLITE_LIMIT_ATTACHED, 1)
+
+      .. versionadded:: 3.11
+
+
 .. _sqlite3-cursor-objects:
 
 Cursor Objects
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 156bfbd9109ee..3500b1ba6b1cb 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -248,6 +248,12 @@ sqlite3
   (Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in
   :issue:`16379`.)
 
+* Add :meth:`~sqlite3.Connection.setlimit` and
+  :meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for
+  setting and getting SQLite limits by connection basis.
+  (Contributed by Erlend E. Aasland in :issue:`45243`.)
+
+
 threading
 ---------
 
diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py
index d82543663d18b..34895be018078 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -167,11 +167,25 @@ def test_module_constants(self):
             "SQLITE_TOOBIG",
             "SQLITE_TRANSACTION",
             "SQLITE_UPDATE",
+            # Run-time limit categories
+            "SQLITE_LIMIT_LENGTH",
+            "SQLITE_LIMIT_SQL_LENGTH",
+            "SQLITE_LIMIT_COLUMN",
+            "SQLITE_LIMIT_EXPR_DEPTH",
+            "SQLITE_LIMIT_COMPOUND_SELECT",
+            "SQLITE_LIMIT_VDBE_OP",
+            "SQLITE_LIMIT_FUNCTION_ARG",
+            "SQLITE_LIMIT_ATTACHED",
+            "SQLITE_LIMIT_LIKE_PATTERN_LENGTH",
+            "SQLITE_LIMIT_VARIABLE_NUMBER",
+            "SQLITE_LIMIT_TRIGGER_DEPTH",
         ]
         if sqlite.sqlite_version_info >= (3, 7, 17):
             consts += ["SQLITE_NOTICE", "SQLITE_WARNING"]
         if sqlite.sqlite_version_info >= (3, 8, 3):
             consts.append("SQLITE_RECURSIVE")
+        if sqlite.sqlite_version_info >= (3, 8, 7):
+            consts.append("SQLITE_LIMIT_WORKER_THREADS")
         consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"]
         for const in consts:
             with self.subTest(const=const):
@@ -332,6 +346,28 @@ def test_drop_unused_refs(self):
             cu = self.cx.execute(f"select {n}")
             self.assertEqual(cu.fetchone()[0], n)
 
+    def test_connection_limits(self):
+        category = sqlite.SQLITE_LIMIT_SQL_LENGTH
+        saved_limit = self.cx.getlimit(category)
+        try:
+            new_limit = 10
+            prev_limit = self.cx.setlimit(category, new_limit)
+            self.assertEqual(saved_limit, prev_limit)
+            self.assertEqual(self.cx.getlimit(category), new_limit)
+            msg = "string or blob too big"
+            self.assertRaisesRegex(sqlite.DataError, msg,
+                                   self.cx.execute, "select 1 as '16'")
+        finally:  # restore saved limit
+            self.cx.setlimit(category, saved_limit)
+
+    def test_connection_bad_limit_category(self):
+        msg = "'category' is out of bounds"
+        cat = 1111
+        self.assertRaisesRegex(sqlite.ProgrammingError, msg,
+                               self.cx.getlimit, cat)
+        self.assertRaisesRegex(sqlite.ProgrammingError, msg,
+                               self.cx.setlimit, cat, 0)
+
 
 class UninitialisedConnectionTests(unittest.TestCase):
     def setUp(self):
@@ -767,6 +803,8 @@ def test_check_connection_thread(self):
             lambda: self.con.set_trace_callback(None),
             lambda: self.con.set_authorizer(None),
             lambda: self.con.create_collation("foo", None),
+            lambda: self.con.setlimit(sqlite.SQLITE_LIMIT_LENGTH, -1),
+            lambda: self.con.getlimit(sqlite.SQLITE_LIMIT_LENGTH),
         ]
         for fn in fns:
             with self.subTest(fn=fn):
diff --git a/Misc/NEWS.d/next/Library/2021-09-20-01-25-09.bpo-45243.0pJf0U.rst b/Misc/NEWS.d/next/Library/2021-09-20-01-25-09.bpo-45243.0pJf0U.rst
new file mode 100644
index 0000000000000..8292e86245f02
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-09-20-01-25-09.bpo-45243.0pJf0U.rst
@@ -0,0 +1,4 @@
+Add :meth:`~sqlite3.Connection.setlimit` and
+:meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for
+setting and getting SQLite limits by connection basis. Patch by Erlend E.
+Aasland.
diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h
index e9e3064ae0f89..f9323eb21d6f4 100644
--- a/Modules/_sqlite/clinic/connection.c.h
+++ b/Modules/_sqlite/clinic/connection.c.h
@@ -750,6 +750,83 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss
     return return_value;
 }
 
+PyDoc_STRVAR(setlimit__doc__,
+"setlimit($self, category, limit, /)\n"
+"--\n"
+"\n"
+"Set connection run-time limits.\n"
+"\n"
+"  category\n"
+"    The limit category to be set.\n"
+"  limit\n"
+"    The new limit. If the new limit is a negative number, the limit is\n"
+"    unchanged.\n"
+"\n"
+"Attempts to increase a limit above its hard upper bound are silently truncated\n"
+"to the hard upper bound. Regardless of whether or not the limit was changed,\n"
+"the prior value of the limit is returned.");
+
+#define SETLIMIT_METHODDEF    \
+    {"setlimit", (PyCFunction)(void(*)(void))setlimit, METH_FASTCALL, setlimit__doc__},
+
+static PyObject *
+setlimit_impl(pysqlite_Connection *self, int category, int limit);
+
+static PyObject *
+setlimit(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    int category;
+    int limit;
+
+    if (!_PyArg_CheckPositional("setlimit", nargs, 2, 2)) {
+        goto exit;
+    }
+    category = _PyLong_AsInt(args[0]);
+    if (category == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    limit = _PyLong_AsInt(args[1]);
+    if (limit == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    return_value = setlimit_impl(self, category, limit);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(getlimit__doc__,
+"getlimit($self, category, /)\n"
+"--\n"
+"\n"
+"Get connection run-time limits.\n"
+"\n"
+"  category\n"
+"    The limit category to be queried.");
+
+#define GETLIMIT_METHODDEF    \
+    {"getlimit", (PyCFunction)getlimit, METH_O, getlimit__doc__},
+
+static PyObject *
+getlimit_impl(pysqlite_Connection *self, int category);
+
+static PyObject *
+getlimit(pysqlite_Connection *self, PyObject *arg)
+{
+    PyObject *return_value = NULL;
+    int category;
+
+    category = _PyLong_AsInt(arg);
+    if (category == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    return_value = getlimit_impl(self, category);
+
+exit:
+    return return_value;
+}
+
 #ifndef PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF
     #define PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF
 #endif /* !defined(PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF) */
@@ -757,4 +834,4 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss
 #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
     #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
 #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */
-/*[clinic end generated code: output=7567e5d716309258 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=0c3901153a3837a5 input=a9049054013a1b77]*/
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index 94c38ad395440..f913267e1560e 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -1896,6 +1896,57 @@ pysqlite_connection_exit_impl(pysqlite_Connection *self, PyObject *exc_type,
     Py_RETURN_FALSE;
 }
 
+/*[clinic input]
+_sqlite3.Connection.setlimit as setlimit
+
+    category: int
+        The limit category to be set.
+    limit: int
+        The new limit. If the new limit is a negative number, the limit is
+        unchanged.
+    /
+
+Set connection run-time limits.
+
+Attempts to increase a limit above its hard upper bound are silently truncated
+to the hard upper bound. Regardless of whether or not the limit was changed,
+the prior value of the limit is returned.
+[clinic start generated code]*/
+
+static PyObject *
+setlimit_impl(pysqlite_Connection *self, int category, int limit)
+/*[clinic end generated code: output=0d208213f8d68ccd input=9bd469537e195635]*/
+{
+    if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
+        return NULL;
+    }
+
+    int old_limit = sqlite3_limit(self->db, category, limit);
+    if (old_limit < 0) {
+        PyErr_SetString(self->ProgrammingError, "'category' is out of bounds");
+        return NULL;
+    }
+    return PyLong_FromLong(old_limit);
+}
+
+/*[clinic input]
+_sqlite3.Connection.getlimit as getlimit
+
+    category: int
+        The limit category to be queried.
+    /
+
+Get connection run-time limits.
+[clinic start generated code]*/
+
+static PyObject *
+getlimit_impl(pysqlite_Connection *self, int category)
+/*[clinic end generated code: output=7c3f5d11f24cecb1 input=61e0849fb4fb058f]*/
+{
+    return setlimit_impl(self, category, -1);
+}
+
+
 static const char connection_doc[] =
 PyDoc_STR("SQLite database connection object.");
 
@@ -1927,6 +1978,8 @@ static PyMethodDef connection_methods[] = {
     PYSQLITE_CONNECTION_SET_AUTHORIZER_METHODDEF
     PYSQLITE_CONNECTION_SET_PROGRESS_HANDLER_METHODDEF
     PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF
+    SETLIMIT_METHODDEF
+    GETLIMIT_METHODDEF
     {NULL, NULL}
 };
 
diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c
index 912851ba775dc..8666af171c8cb 100644
--- a/Modules/_sqlite/module.c
+++ b/Modules/_sqlite/module.c
@@ -395,6 +395,21 @@ add_integer_constants(PyObject *module) {
     ADD_INT(SQLITE_SAVEPOINT);
 #if SQLITE_VERSION_NUMBER >= 3008003
     ADD_INT(SQLITE_RECURSIVE);
+#endif
+    // Run-time limit categories
+    ADD_INT(SQLITE_LIMIT_LENGTH);
+    ADD_INT(SQLITE_LIMIT_SQL_LENGTH);
+    ADD_INT(SQLITE_LIMIT_COLUMN);
+    ADD_INT(SQLITE_LIMIT_EXPR_DEPTH);
+    ADD_INT(SQLITE_LIMIT_COMPOUND_SELECT);
+    ADD_INT(SQLITE_LIMIT_VDBE_OP);
+    ADD_INT(SQLITE_LIMIT_FUNCTION_ARG);
+    ADD_INT(SQLITE_LIMIT_ATTACHED);
+    ADD_INT(SQLITE_LIMIT_LIKE_PATTERN_LENGTH);
+    ADD_INT(SQLITE_LIMIT_VARIABLE_NUMBER);
+    ADD_INT(SQLITE_LIMIT_TRIGGER_DEPTH);
+#if SQLITE_VERSION_NUMBER >= 3008007
+    ADD_INT(SQLITE_LIMIT_WORKER_THREADS);
 #endif
 #undef ADD_INT
     return 0;



More information about the Python-checkins mailing list