From 9bf6b0637d04aadd51b81db69f7ca4692d9c20a3 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 13 Jul 2026 18:42:21 +0800 Subject: [PATCH 1/2] gh-153658: Fix sqlite3 iterdump() for table names containing a single quote Patch by tonghuaroot. --- Lib/sqlite3/dump.py | 10 +++++++--- Lib/test/test_sqlite3/test_dump.py | 18 ++++++++++++++++++ ...6-07-13-10-27-08.gh-issue-153658.A6o4n5.rst | 2 ++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-13-10-27-08.gh-issue-153658.A6o4n5.rst diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index 57e6a3b4f1e6eba..d39cebb3e25d23b 100644 --- a/Lib/sqlite3/dump.py +++ b/Lib/sqlite3/dump.py @@ -80,11 +80,15 @@ def _iterdump(connection, *, filter=None): table_name_ident = _quote_name(table_name) 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( - table_name_ident, + # In the string-literal copy any single quote must be doubled; + # the FROM clause below uses the identifier form as-is. + table_name_literal = table_name_ident.replace("'", "''") + q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {2};".format( + table_name_literal, "','".join( "||quote({0})||".format(_quote_name(col)) for col in column_names - ) + ), + table_name_ident, ) 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 e18a207e9f64949..7841b610cd222a7 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -56,6 +56,24 @@ def test_table_dump(self): [self.assertEqual(expected_sqls[i], actual_sqls[i]) for i in range(len(expected_sqls))] + def test_dump_single_quote_in_identifier(self): + # A single quote in a table or column name must not break the dump. + self.cu.execute("""CREATE TABLE "a'b" ("c'd" text);""") + self.cu.execute("""INSERT INTO "a'b" VALUES('x''y');""") + expected = [ + "BEGIN TRANSACTION;", + """CREATE TABLE "a'b" ("c'd" text);""", + """INSERT INTO "a'b" VALUES('x''y');""", + "COMMIT;", + ] + actual = list(self.cx.iterdump()) + self.assertEqual(expected, actual) + # The dump restores into a fresh database. + with memory_database() as cx2: + cx2.executescript("".join(actual)) + row = cx2.execute("""SELECT "c'd" FROM "a'b";""").fetchone() + self.assertEqual(row[0], "x'y") + def test_table_dump_filter(self): all_table_sqls = [ """CREATE TABLE "some_table_2" ("id_1" INTEGER);""", diff --git a/Misc/NEWS.d/next/Library/2026-07-13-10-27-08.gh-issue-153658.A6o4n5.rst b/Misc/NEWS.d/next/Library/2026-07-13-10-27-08.gh-issue-153658.A6o4n5.rst new file mode 100644 index 000000000000000..06385bb7754b127 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-13-10-27-08.gh-issue-153658.A6o4n5.rst @@ -0,0 +1,2 @@ +Fix :meth:`sqlite3.Connection.iterdump` raising :exc:`sqlite3.OperationalError` +when a table name contains a single quote. Patch by tonghuaroot. From e05fdd4fe82b309c37397e1b720aa1cfc7cf6bd7 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Tue, 14 Jul 2026 09:46:24 +0800 Subject: [PATCH 2/2] Extract _escape_single_quotes() helper for the string-literal escaping --- Lib/sqlite3/dump.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index d39cebb3e25d23b..95e14a6655ef172 100644 --- a/Lib/sqlite3/dump.py +++ b/Lib/sqlite3/dump.py @@ -11,8 +11,12 @@ def _quote_name(name): return '"{0}"'.format(name.replace('"', '""')) +def _escape_single_quotes(value): + return value.replace("'", "''") + + def _quote_value(value): - return "'{0}'".format(value.replace("'", "''")) + return "'{0}'".format(_escape_single_quotes(value)) def _iterdump(connection, *, filter=None): @@ -80,11 +84,8 @@ def _iterdump(connection, *, filter=None): table_name_ident = _quote_name(table_name) res = cu.execute(f'PRAGMA table_info({table_name_ident})') column_names = [str(table_info[1]) for table_info in res.fetchall()] - # In the string-literal copy any single quote must be doubled; - # the FROM clause below uses the identifier form as-is. - table_name_literal = table_name_ident.replace("'", "''") q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {2};".format( - table_name_literal, + _escape_single_quotes(table_name_ident), "','".join( "||quote({0})||".format(_quote_name(col)) for col in column_names ),