Restore previous legacy_alter_table value after transform

transform_sql() now reads the connection's current
PRAGMA legacy_alter_table value and emits a closing pragma that
restores it, instead of always resetting it to OFF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-08-06 22:15:57 -07:00
commit 4b66d515a3
3 changed files with 26 additions and 3 deletions

View file

@ -1995,7 +1995,7 @@ Tables that are referenced by views can be safely transformed - the view definit
A view that references a column which the transform renamed or dropped will remain defined but will raise a ``no such column`` error when it is next queried. This is inherent to SQLite views, whose SQL is stored as text - if you rename or drop columns that a view depends on you should update that view definition yourself.
To achieve this, the SQL produced by ``transform_sql()`` brackets its ``ALTER TABLE ... RENAME TO`` statements with ``PRAGMA legacy_alter_table=ON`` and ``PRAGMA legacy_alter_table=OFF`` - without this, SQLite would attempt to rewrite references to the renamed table in every view definition, which fails when a view references the table that was just dropped. One consequence is that ``PRAGMA legacy_alter_table`` is reset to ``OFF`` (the SQLite default) after a transform, even if it was previously set to ``ON`` for the connection.
To achieve this, the SQL produced by ``transform_sql()`` turns on ``PRAGMA legacy_alter_table`` for its ``ALTER TABLE ... RENAME TO`` statements, then restores the pragma to the value it had when the SQL was generated - without this, SQLite would attempt to rewrite references to the renamed table in every view definition, which fails when a view references the table that was just dropped.
.. _python_api_transform_sql:

View file

@ -2830,7 +2830,12 @@ class Table(Queryable):
# references the table that was just dropped - and with keep_table=
# would silently repoint views at the backup table. These renames are
# an implementation detail of transform(), so use legacy_alter_table
# to leave view definitions untouched.
# to leave view definitions untouched, restoring the connection's
# current value afterwards.
legacy_alter_table_row = self.db.execute("PRAGMA legacy_alter_table").fetchone()
legacy_alter_table_was_on = bool(
legacy_alter_table_row and legacy_alter_table_row[0]
)
if keep_table:
sqls.append("PRAGMA legacy_alter_table=ON;")
sqls.append(
@ -2842,7 +2847,11 @@ class Table(Queryable):
sqls.append(
f"ALTER TABLE {quote_identifier(new_table_name)} RENAME TO {quote_identifier(self.name)};"
)
sqls.append("PRAGMA legacy_alter_table=OFF;")
sqls.append(
"PRAGMA legacy_alter_table={};".format(
"ON" if legacy_alter_table_was_on else "OFF"
)
)
# Re-add existing indexes
for index in self.indexes:
if index.origin != "pk":

View file

@ -1049,3 +1049,17 @@ def test_transform_with_view_in_open_transaction(fresh_db):
"select sql from sqlite_master where name = 'dogs_view'"
).fetchone()[0]
assert view_sql == "CREATE VIEW dogs_view as select id, name from dogs"
def test_transform_restores_legacy_alter_table_setting(fresh_db):
dogs = fresh_db["dogs"]
dogs.insert({"id": 1, "name": "Cleo"}, pk="id")
# Default is OFF, reset to OFF afterwards
dogs.transform(types={"name": str})
assert fresh_db.execute("PRAGMA legacy_alter_table").fetchone()[0] == 0
# If the connection has it ON, it should be restored to ON
fresh_db.execute("PRAGMA legacy_alter_table=ON")
sqls = dogs.transform_sql(types={"name": str}, tmp_suffix="suffix")
assert sqls[-1] == "PRAGMA legacy_alter_table=ON;"
dogs.transform(types={"name": str})
assert fresh_db.execute("PRAGMA legacy_alter_table").fetchone()[0] == 1