summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2024-02-06 11:34:56 (GMT)
committerGitHub <noreply@github.com>2024-02-06 11:34:56 (GMT)
commit1a10437a14b13100bdf41cbdab819c33258deb65 (patch)
tree1810d94e1a64b264d28865f5cab1cf08b13c38ea
parent4bf41879d03b1da3c6d38c39a04331e3ae2e7545 (diff)
downloadcpython-1a10437a14b13100bdf41cbdab819c33258deb65.zip
cpython-1a10437a14b13100bdf41cbdab819c33258deb65.tar.gz
cpython-1a10437a14b13100bdf41cbdab819c33258deb65.tar.bz2
gh-91602: Add iterdump() support for filtering database objects (#114501)
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>
-rw-r--r--Doc/library/sqlite3.rst11
-rw-r--r--Doc/whatsnew/3.13.rst4
-rw-r--r--Include/internal/pycore_global_objects_fini_generated.h1
-rw-r--r--Include/internal/pycore_global_strings.h1
-rw-r--r--Include/internal/pycore_runtime_init_generated.h1
-rw-r--r--Include/internal/pycore_unicodeobject_generated.h3
-rw-r--r--Lib/sqlite3/dump.py19
-rw-r--r--Lib/test/test_sqlite3/test_dump.py70
-rw-r--r--Misc/NEWS.d/next/Library/2024-01-24-20-51-49.gh-issue-91602.8fOH8l.rst3
-rw-r--r--Modules/_sqlite/clinic/connection.c.h60
-rw-r--r--Modules/_sqlite/connection.c20
11 files changed, 176 insertions, 17 deletions
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index c3406b1..87d5ef1 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -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)
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 5e5f1e2..3727577 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
----------
diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index dd09ff4..932738c 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -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));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index 79d6509..da62b4f 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -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)
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index f3c55ac..68fbbcb 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -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), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index 2e95723..c8458b4 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -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);
diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py
index 719dfc8..9dcce7d 100644
--- a/Lib/sqlite3/dump.py
+++ b/Lib/sqlite3/dump.py
@@ -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))
diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py
index 2e1f0b8..7261b7f 100644
--- a/Lib/test/test_sqlite3/test_dump.py
+++ b/Lib/test/test_sqlite3/test_dump.py
@@ -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
index 0000000..21d39df
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-01-24-20-51-49.gh-issue-91602.8fOH8l.rst
@@ -0,0 +1,3 @@
+Add *filter* keyword-only parameter to
+:meth:`sqlite3.Connection.iterdump` for filtering database objects to dump.
+Patch by Mariusz Felisiak.
diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h
index f2cff6a..811314b 100644
--- a/Modules/_sqlite/clinic/connection.c.h
+++ b/Modules/_sqlite/clinic/connection.c.h
@@ -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]*/
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index 0a66339..f97afcf 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -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;
}