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() 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.