From 774d467abb2aa6e33e40e43d990407483ad82922 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Tue, 14 Jul 2026 20:00:16 +0800 Subject: [PATCH 1/2] gh-153729: Fix sqlite3 iterdump() for populated virtual tables --- Lib/sqlite3/dump.py | 2 ++ Lib/test/test_sqlite3/test_dump.py | 31 +++++++++++++++++++ ...-07-14-10-00-00.gh-issue-153729.p3yD9r.rst | 3 ++ 3 files changed, 36 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-14-10-00-00.gh-issue-153729.p3yD9r.rst diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index 95e14a6655ef17..5cc0d9a1a9d124 100644 --- a/Lib/sqlite3/dump.py +++ b/Lib/sqlite3/dump.py @@ -77,6 +77,8 @@ def _iterdump(connection, *, filter=None): _quote_value(table_name), _quote_value(sql), )) + # Rows live in the shadow tables, dumped separately. + continue else: yield('{0};'.format(sql)) diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py index 7841b610cd222a..eb19e84654bf5e 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -1,7 +1,10 @@ # Author: Paul Kippes +import sqlite3 import unittest +from test.support.os_helper import TESTFN, unlink + from .util import memory_database from .util import MemoryDatabaseMixin from .util import requires_virtual_table @@ -246,6 +249,34 @@ def test_dump_virtual_tables(self): actual = list(self.cx.iterdump()) self.assertEqual(expected, actual) + @requires_virtual_table("fts4") + def test_dump_virtual_table_data_roundtrip(self): + # gh-153729: a populated virtual table must round-trip through iterdump(). + self.addCleanup(unlink, TESTFN) + with sqlite3.connect(TESTFN) as src: + src.execute("CREATE VIRTUAL TABLE test USING fts4(example)") + src.execute("INSERT INTO test(example) VALUES('hello world')") + src.execute("INSERT INTO test(example) VALUES('second row')") + src.commit() + script = "".join(src.iterdump()) + + # The virtual table's own rows are not dumped as INSERT statements + # (the data is preserved via the shadow tables instead). + self.assertNotIn('INSERT INTO "test"', script) + + restored_path = f"{TESTFN}.restored" + self.addCleanup(unlink, restored_path) + with sqlite3.connect(restored_path) as dst: + # Defensive mode blocks writable_schema writes to sqlite_master. + dst.setconfig(sqlite3.SQLITE_DBCONFIG_DEFENSIVE, False) + dst.setconfig(sqlite3.SQLITE_DBCONFIG_WRITABLE_SCHEMA, True) + dst.executescript(script) + with sqlite3.connect(restored_path) as restored: + rows = restored.execute( + "SELECT example FROM test ORDER BY docid" + ).fetchall() + self.assertEqual(rows, [("hello world",), ("second row",)]) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-14-10-00-00.gh-issue-153729.p3yD9r.rst b/Misc/NEWS.d/next/Library/2026-07-14-10-00-00.gh-issue-153729.p3yD9r.rst new file mode 100644 index 00000000000000..88004303345fca --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-14-10-00-00.gh-issue-153729.p3yD9r.rst @@ -0,0 +1,3 @@ +:meth:`sqlite3.Connection.iterdump` no longer emits ``INSERT`` statements for +a virtual table's own rows, which previously made restoring the dump raise +:exc:`sqlite3.OperationalError`. Patch by tonghuaroot. From b75d18d72beae0d7f63c67731713b8d69b16f538 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Wed, 15 Jul 2026 10:10:12 +0800 Subject: [PATCH 2/2] Close the connections opened by the iterdump virtual-table test --- Lib/test/test_sqlite3/test_dump.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py index eb19e84654bf5e..4cde5f891ad316 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -2,6 +2,7 @@ import sqlite3 import unittest +from contextlib import closing from test.support.os_helper import TESTFN, unlink @@ -253,7 +254,7 @@ def test_dump_virtual_tables(self): def test_dump_virtual_table_data_roundtrip(self): # gh-153729: a populated virtual table must round-trip through iterdump(). self.addCleanup(unlink, TESTFN) - with sqlite3.connect(TESTFN) as src: + with closing(sqlite3.connect(TESTFN)) as src: src.execute("CREATE VIRTUAL TABLE test USING fts4(example)") src.execute("INSERT INTO test(example) VALUES('hello world')") src.execute("INSERT INTO test(example) VALUES('second row')") @@ -266,12 +267,12 @@ def test_dump_virtual_table_data_roundtrip(self): restored_path = f"{TESTFN}.restored" self.addCleanup(unlink, restored_path) - with sqlite3.connect(restored_path) as dst: + with closing(sqlite3.connect(restored_path)) as dst: # Defensive mode blocks writable_schema writes to sqlite_master. dst.setconfig(sqlite3.SQLITE_DBCONFIG_DEFENSIVE, False) dst.setconfig(sqlite3.SQLITE_DBCONFIG_WRITABLE_SCHEMA, True) dst.executescript(script) - with sqlite3.connect(restored_path) as restored: + with closing(sqlite3.connect(restored_path)) as restored: rows = restored.execute( "SELECT example FROM test ORDER BY docid" ).fetchall()