From 6738fab2fb5e2afb5bcc5a9724d6f0d610018aa2 Mon Sep 17 00:00:00 2001 From: aradhyasjoshi Date: Mon, 13 Jul 2026 21:34:13 +0530 Subject: [PATCH 1/2] bpo-153658: sqlite3.Connection.iterdump(): escape single quotes in identifiers embedded in string literals\n\nProperly double single quotes when embedding a double-quoted identifier into a single-quoted SQL string literal so table/column names with apostrophes round-trip.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Lib/sqlite3/dump.py | 16 ++++++++++++---- Lib/test/test_sqlite3/test_dump.py | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index 57e6a3b4f1e6eb..b34803cefe9e6e 100644 --- a/Lib/sqlite3/dump.py +++ b/Lib/sqlite3/dump.py @@ -78,13 +78,21 @@ def _iterdump(connection, *, filter=None): # Build the insert statement for each row of the current table table_name_ident = _quote_name(table_name) + # When the identifier (which may contain single quotes) is embedded + # inside a single-quoted SQL string literal below, any single quotes + # must be doubled so the string literal remains valid. Use + # table_name_ident for identifier contexts (PRAGMA, FROM), and + # table_name_literal for the string-literal context. + table_name_literal = table_name_ident.replace("'", "''") res = cu.execute(f'PRAGMA table_info({table_name_ident})') column_names = [str(table_info[1]) for table_info in res.fetchall()] - q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {0};".format( + cols = "','".join( + "||quote({0})||".format(_quote_name(col)) for col in column_names + ) + q = "SELECT 'INSERT INTO {1} VALUES('{2}')' FROM {0};".format( table_name_ident, - "','".join( - "||quote({0})||".format(_quote_name(col)) for col in column_names - ) + table_name_literal, + cols, ) query_res = cu.execute(q) for row in query_res: diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py index e18a207e9f6494..a02809fe132284 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -228,6 +228,28 @@ def test_dump_virtual_tables(self): actual = list(self.cx.iterdump()) self.assertEqual(expected, actual) + def test_dump_table_with_single_quote_in_name(self): + # Table names containing single quotes must be round-trippable. + self.cu.execute("CREATE TABLE \"a'b\" (x);") + self.cu.execute("INSERT INTO \"a'b\" VALUES (1);") + dump_sqls = list(self.cx.iterdump()) + with memory_database() as cx2: + cx2.executescript("".join(dump_sqls)) + cu2 = cx2.cursor() + res = cu2.execute("SELECT * FROM \"a'b\";").fetchall() + self.assertEqual(res, [(1,)]) + + def test_dump_table_with_double_quotes_in_name(self): + # Regression test: names with double quotes already handled. + self.cu.execute('CREATE TABLE "a""b" (x)') + self.cu.execute('INSERT INTO "a""b" VALUES (2)') + dump_sqls = list(self.cx.iterdump()) + with memory_database() as cx2: + cx2.executescript("".join(dump_sqls)) + cu2 = cx2.cursor() + res = cu2.execute('SELECT * FROM "a""b";').fetchall() + self.assertEqual(res, [(2,)]) + if __name__ == "__main__": unittest.main() From 2415ff8b75c3f6a784df4ade903c31013e088114 Mon Sep 17 00:00:00 2001 From: aradhyasjoshi Date: Mon, 13 Jul 2026 21:39:47 +0530 Subject: [PATCH 2/2] bpo-153658: Add NEWS entry for sqlite3 iterdump() fix\n\nAdd NEWS fragment for bpo-153658 so the change is recorded in Misc/NEWS.d. --- Misc/NEWS.d/153658.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/153658.rst diff --git a/Misc/NEWS.d/153658.rst b/Misc/NEWS.d/153658.rst new file mode 100644 index 00000000000000..be9640301386a9 --- /dev/null +++ b/Misc/NEWS.d/153658.rst @@ -0,0 +1,3 @@ +bpo-153658: sqlite3.Connection.iterdump() now correctly handles table and column names that contain single quotes (apostrophes). Identifiers with apostrophes are properly escaped when embedded in single-quoted SQL string literals, so dumps produced by iterdump() can be replayed with executescript(). + +Contributed by @joshiaradhya.