]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-91602: Add iterdump() support for filtering database objects (#114501)
authorMariusz Felisiak <felisiak.mariusz@gmail.com>
Tue, 6 Feb 2024 11:34:56 +0000 (12:34 +0100)
committerGitHub <noreply@github.com>
Tue, 6 Feb 2024 11:34:56 +0000 (12:34 +0100)
Add optional 'filter' parameter to iterdump() that allows a "LIKE"
pattern for filtering database objects to dump.

Co-authored-by: Erlend E. Aasland <erlend@python.org>
Doc/library/sqlite3.rst
Doc/whatsnew/3.13.rst
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/sqlite3/dump.py
Lib/test/test_sqlite3/test_dump.py
Misc/NEWS.d/next/Library/2024-01-24-20-51-49.gh-issue-91602.8fOH8l.rst [new file with mode: 0644]
Modules/_sqlite/clinic/connection.c.h
Modules/_sqlite/connection.c

index c3406b166c3d89d6bd254e04e4799afc6fbc2217..87d5ef1e42ca3acb499e0ac34a428481b8570e9c 100644 (file)
@@ -1137,12 +1137,19 @@ Connection objects
 
    .. _Loading an Extension: https://www.sqlite.org/loadext.html#loading_an_extension_
 
-   .. method:: iterdump
+   .. method:: iterdump(*, filter=None)
 
       Return an :term:`iterator` to dump the database as SQL source code.
       Useful when saving an in-memory database for later restoration.
       Similar to the ``.dump`` command in the :program:`sqlite3` shell.
 
+      :param filter:
+
+        An optional ``LIKE`` pattern for database objects to dump, e.g. ``prefix_%``.
+        If ``None`` (the default), all database objects will be included.
+
+      :type filter: str | None
+
       Example:
 
       .. testcode::
@@ -1158,6 +1165,8 @@ Connection objects
 
          :ref:`sqlite3-howto-encoding`
 
+      .. versionchanged:: 3.13
+         Added the *filter* parameter.
 
    .. method:: backup(target, *, pages=-1, progress=None, name="main", sleep=0.250)
 
index 5e5f1e295f4d706de5a705faf7150f7f4719d89e..372757759b986f938d8dc67131d17bf2791732cf 100644 (file)
@@ -438,6 +438,10 @@ sqlite3
   object is not :meth:`closed <sqlite3.Connection.close>` explicitly.
   (Contributed by Erlend E. Aasland in :gh:`105539`.)
 
+* Add *filter* keyword-only parameter to :meth:`sqlite3.Connection.iterdump`
+  for filtering database objects to dump.
+  (Contributed by Mariusz Felisiak in :gh:`91602`.)
+
 subprocess
 ----------
 
index dd09ff40f39fe6fbcb57d8a669b110eaf5e2e0b2..932738c3049882b9f209a2721da4013b33fbbd2f 100644 (file)
@@ -940,6 +940,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fileno));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(filepath));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fillvalue));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(filter));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(filters));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(final));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(find_class));
index 79d6509abcdfd91c1663c60533ee55e18b4153ef..da62b4f0a951ff877e171d8e85cd38d6eb1e3ef3 100644 (file)
@@ -429,6 +429,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(fileno)
         STRUCT_FOR_ID(filepath)
         STRUCT_FOR_ID(fillvalue)
+        STRUCT_FOR_ID(filter)
         STRUCT_FOR_ID(filters)
         STRUCT_FOR_ID(final)
         STRUCT_FOR_ID(find_class)
index f3c55acfb3c282f6c8f9d85eb8a2f8641fe712a2..68fbbcb4378e17fde06d890725a0715502aa20e5 100644 (file)
@@ -938,6 +938,7 @@ extern "C" {
     INIT_ID(fileno), \
     INIT_ID(filepath), \
     INIT_ID(fillvalue), \
+    INIT_ID(filter), \
     INIT_ID(filters), \
     INIT_ID(final), \
     INIT_ID(find_class), \
index 2e9572382fe03327d4a923bade2c5f59c18b8d90..c8458b4e36ccc93ad2c1cc38cc36be01530af1f9 100644 (file)
@@ -1128,6 +1128,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     string = &_Py_ID(fillvalue);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
+    string = &_Py_ID(filter);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    _PyUnicode_InternInPlace(interp, &string);
     string = &_Py_ID(filters);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
index 719dfc8947697d44035ee8e1195313d38e8e5abf..9dcce7dc76ced4880f233908946ed2ba59cf29c8 100644 (file)
@@ -15,7 +15,7 @@ def _quote_value(value):
     return "'{0}'".format(value.replace("'", "''"))
 
 
-def _iterdump(connection):
+def _iterdump(connection, *, filter=None):
     """
     Returns an iterator to the dump of the database in an SQL text format.
 
@@ -32,15 +32,23 @@ def _iterdump(connection):
         yield('PRAGMA foreign_keys=OFF;')
     yield('BEGIN TRANSACTION;')
 
+    if filter:
+        # Return database objects which match the filter pattern.
+        filter_name_clause = 'AND "name" LIKE ?'
+        params = [filter]
+    else:
+        filter_name_clause = ""
+        params = []
     # sqlite_master table contains the SQL CREATE statements for the database.
-    q = """
+    q = f"""
         SELECT "name", "type", "sql"
         FROM "sqlite_master"
             WHERE "sql" NOT NULL AND
             "type" == 'table'
+            {filter_name_clause}
             ORDER BY "name"
         """
-    schema_res = cu.execute(q)
+    schema_res = cu.execute(q, params)
     sqlite_sequence = []
     for table_name, type, sql in schema_res.fetchall():
         if table_name == 'sqlite_sequence':
@@ -82,13 +90,14 @@ def _iterdump(connection):
             yield("{0};".format(row[0]))
 
     # Now when the type is 'index', 'trigger', or 'view'
-    q = """
+    q = f"""
         SELECT "name", "type", "sql"
         FROM "sqlite_master"
             WHERE "sql" NOT NULL AND
             "type" IN ('index', 'trigger', 'view')
+            {filter_name_clause}
         """
-    schema_res = cu.execute(q)
+    schema_res = cu.execute(q, params)
     for name, type, sql in schema_res.fetchall():
         yield('{0};'.format(sql))
 
index 2e1f0b80c10f46e70b1ba4a2480cb79147629210..7261b7f0dc93d0ab4e117b8acf6c3d7b9761e724 100644 (file)
@@ -54,6 +54,76 @@ class DumpTests(MemoryDatabaseMixin, unittest.TestCase):
         [self.assertEqual(expected_sqls[i], actual_sqls[i])
             for i in range(len(expected_sqls))]
 
+    def test_table_dump_filter(self):
+        all_table_sqls = [
+            """CREATE TABLE "some_table_2" ("id_1" INTEGER);""",
+            """INSERT INTO "some_table_2" VALUES(3);""",
+            """INSERT INTO "some_table_2" VALUES(4);""",
+            """CREATE TABLE "test_table_1" ("id_2" INTEGER);""",
+            """INSERT INTO "test_table_1" VALUES(1);""",
+            """INSERT INTO "test_table_1" VALUES(2);""",
+        ]
+        all_views_sqls = [
+            """CREATE VIEW "view_1" AS SELECT * FROM "some_table_2";""",
+            """CREATE VIEW "view_2" AS SELECT * FROM "test_table_1";""",
+        ]
+        # Create database structure.
+        for sql in [*all_table_sqls, *all_views_sqls]:
+            self.cu.execute(sql)
+        # %_table_% matches all tables.
+        dump_sqls = list(self.cx.iterdump(filter="%_table_%"))
+        self.assertEqual(
+            dump_sqls,
+            ["BEGIN TRANSACTION;", *all_table_sqls, "COMMIT;"],
+        )
+        # view_% matches all views.
+        dump_sqls = list(self.cx.iterdump(filter="view_%"))
+        self.assertEqual(
+            dump_sqls,
+            ["BEGIN TRANSACTION;", *all_views_sqls, "COMMIT;"],
+        )
+        # %_1 matches tables and views with the _1 suffix.
+        dump_sqls = list(self.cx.iterdump(filter="%_1"))
+        self.assertEqual(
+            dump_sqls,
+            [
+                "BEGIN TRANSACTION;",
+                """CREATE TABLE "test_table_1" ("id_2" INTEGER);""",
+                """INSERT INTO "test_table_1" VALUES(1);""",
+                """INSERT INTO "test_table_1" VALUES(2);""",
+                """CREATE VIEW "view_1" AS SELECT * FROM "some_table_2";""",
+                "COMMIT;"
+            ],
+        )
+        # some_% matches some_table_2.
+        dump_sqls = list(self.cx.iterdump(filter="some_%"))
+        self.assertEqual(
+            dump_sqls,
+            [
+                "BEGIN TRANSACTION;",
+                """CREATE TABLE "some_table_2" ("id_1" INTEGER);""",
+                """INSERT INTO "some_table_2" VALUES(3);""",
+                """INSERT INTO "some_table_2" VALUES(4);""",
+                "COMMIT;"
+            ],
+        )
+        # Only single object.
+        dump_sqls = list(self.cx.iterdump(filter="view_2"))
+        self.assertEqual(
+            dump_sqls,
+            [
+                "BEGIN TRANSACTION;",
+                """CREATE VIEW "view_2" AS SELECT * FROM "test_table_1";""",
+                "COMMIT;"
+            ],
+        )
+        # % matches all objects.
+        dump_sqls = list(self.cx.iterdump(filter="%"))
+        self.assertEqual(
+            dump_sqls,
+            ["BEGIN TRANSACTION;", *all_table_sqls, *all_views_sqls, "COMMIT;"],
+        )
+
     def test_dump_autoincrement(self):
         expected = [
             'CREATE TABLE "t1" (id integer primary key autoincrement);',
diff --git a/Misc/NEWS.d/next/Library/2024-01-24-20-51-49.gh-issue-91602.8fOH8l.rst b/Misc/NEWS.d/next/Library/2024-01-24-20-51-49.gh-issue-91602.8fOH8l.rst
new file mode 100644 (file)
index 0000000..21d39df
--- /dev/null
@@ -0,0 +1,3 @@
+Add *filter* keyword-only parameter to
+:meth:`sqlite3.Connection.iterdump` for filtering database objects to dump.
+Patch by Mariusz Felisiak.
index f2cff6a7b421f3b5bb78b80b53cb45678d5b1c54..811314b5cd8aed8d60c3d34086ef37381c1dec0d 100644 (file)
@@ -1204,21 +1204,67 @@ pysqlite_connection_interrupt(pysqlite_Connection *self, PyObject *Py_UNUSED(ign
 }
 
 PyDoc_STRVAR(pysqlite_connection_iterdump__doc__,
-"iterdump($self, /)\n"
+"iterdump($self, /, *, filter=None)\n"
 "--\n"
 "\n"
-"Returns iterator to the dump of the database in an SQL text format.");
+"Returns iterator to the dump of the database in an SQL text format.\n"
+"\n"
+"  filter\n"
+"    An optional LIKE pattern for database objects to dump");
 
 #define PYSQLITE_CONNECTION_ITERDUMP_METHODDEF    \
-    {"iterdump", (PyCFunction)pysqlite_connection_iterdump, METH_NOARGS, pysqlite_connection_iterdump__doc__},
+    {"iterdump", _PyCFunction_CAST(pysqlite_connection_iterdump), METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_iterdump__doc__},
 
 static PyObject *
-pysqlite_connection_iterdump_impl(pysqlite_Connection *self);
+pysqlite_connection_iterdump_impl(pysqlite_Connection *self,
+                                  PyObject *filter);
 
 static PyObject *
-pysqlite_connection_iterdump(pysqlite_Connection *self, PyObject *Py_UNUSED(ignored))
+pysqlite_connection_iterdump(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
 {
-    return pysqlite_connection_iterdump_impl(self);
+    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(filter), },
+    };
+    #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[] = {"filter", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "iterdump",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
+    PyObject *filter = Py_None;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 0, 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    filter = args[0];
+skip_optional_kwonly:
+    return_value = pysqlite_connection_iterdump_impl(self, filter);
+
+exit:
+    return return_value;
 }
 
 PyDoc_STRVAR(pysqlite_connection_backup__doc__,
@@ -1820,4 +1866,4 @@ exit:
 #ifndef DESERIALIZE_METHODDEF
     #define DESERIALIZE_METHODDEF
 #endif /* !defined(DESERIALIZE_METHODDEF) */
-/*[clinic end generated code: output=99299d3ee2c247ab input=a9049054013a1b77]*/
+/*[clinic end generated code: output=3c6d0b748fac016f input=a9049054013a1b77]*/
index 0a6633972cc5ef777c9ff1acd85c0e11e4a3f2b1..f97afcf5fcf16ef16768d382f580d71cf77c7b05 100644 (file)
@@ -1979,12 +1979,17 @@ finally:
 /*[clinic input]
 _sqlite3.Connection.iterdump as pysqlite_connection_iterdump
 
+    *
+    filter: object = None
+        An optional LIKE pattern for database objects to dump
+
 Returns iterator to the dump of the database in an SQL text format.
 [clinic start generated code]*/
 
 static PyObject *
-pysqlite_connection_iterdump_impl(pysqlite_Connection *self)
-/*[clinic end generated code: output=586997aaf9808768 input=1911ca756066da89]*/
+pysqlite_connection_iterdump_impl(pysqlite_Connection *self,
+                                  PyObject *filter)
+/*[clinic end generated code: output=fd81069c4bdeb6b0 input=4ae6d9a898f108df]*/
 {
     if (!pysqlite_check_connection(self)) {
         return NULL;
@@ -1998,9 +2003,16 @@ pysqlite_connection_iterdump_impl(pysqlite_Connection *self)
         }
         return NULL;
     }
-
-    PyObject *retval = PyObject_CallOneArg(iterdump, (PyObject *)self);
+    PyObject *args[3] = {NULL, (PyObject *)self, filter};
+    PyObject *kwnames = Py_BuildValue("(s)", "filter");
+    if (!kwnames) {
+        Py_DECREF(iterdump);
+        return NULL;
+    }
+    Py_ssize_t nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET;
+    PyObject *retval = PyObject_Vectorcall(iterdump, args + 1, nargsf, kwnames);
     Py_DECREF(iterdump);
+    Py_DECREF(kwnames);
     return retval;
 }