diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py index 57e6a3b4f1e6eba..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,12 @@ 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, + q = "SELECT 'INSERT INTO {0} VALUES('{1}')' FROM {2};".format( + _escape_single_quotes(table_name_ident), "','".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.