mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-26 10:54:32 +02:00
Transform now refuses to run inside a transaction if destructive foreign keys exist
PRAGMA foreign_keys cannot be changed inside a transaction, and the defer_foreign_keys fallback only defers violation checks, not ON DELETE actions - so calling table.transform() inside db.atomic() with PRAGMA foreign_keys enabled would silently fire ON DELETE CASCADE / SET NULL / SET DEFAULT actions on referencing tables when the old table is dropped. transform() now raises TransactionError in that situation, naming the offending foreign keys and suggesting either running it outside the transaction or turning the pragma off before opening it. Tables with only non-destructive inbound foreign keys (NO ACTION, RESTRICT) can still be transformed inside a transaction via defer_foreign_keys. Closes #794 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014StVTWQJpFhfZJK2CYVBwv
This commit is contained in:
parent
d714200659
commit
44dd92a12c
3 changed files with 190 additions and 2 deletions
|
|
@ -2522,6 +2522,11 @@ class Table(Queryable):
|
|||
|
||||
See :ref:`python_api_transform` for full details.
|
||||
|
||||
Raises :py:class:`sqlite_utils.db.TransactionError` if called while a
|
||||
transaction is open with ``PRAGMA foreign_keys`` enabled and the table
|
||||
is referenced by foreign keys with destructive ``ON DELETE`` actions -
|
||||
see :ref:`python_api_transform_foreign_keys_transactions`.
|
||||
|
||||
:param types: Columns that should have their type changed, for example ``{"weight": float}``
|
||||
:param rename: Columns to rename, for example ``{"headline": "title"}``
|
||||
:param drop: Columns to drop
|
||||
|
|
@ -2566,6 +2571,36 @@ class Table(Queryable):
|
|||
should_defer_foreign_keys = (
|
||||
pragma_foreign_keys_was_on and already_in_transaction
|
||||
)
|
||||
if should_defer_foreign_keys:
|
||||
# PRAGMA foreign_keys is a no-op inside a transaction, and
|
||||
# defer_foreign_keys only defers violation checks, not ON DELETE
|
||||
# actions - so dropping the old table would still fire destructive
|
||||
# actions on any tables that reference it. Refuse rather than
|
||||
# silently modify or delete those rows.
|
||||
destructive_fks = [
|
||||
(table.name, fk)
|
||||
for table in self.db.tables
|
||||
for fk in table.foreign_keys
|
||||
if fk.other_table == self.name
|
||||
and fk.on_delete in ("CASCADE", "SET NULL", "SET DEFAULT")
|
||||
]
|
||||
if destructive_fks:
|
||||
raise TransactionError(
|
||||
"Cannot transform table {table} while a transaction is open: "
|
||||
"PRAGMA foreign_keys cannot be changed inside a transaction, "
|
||||
"and the table is referenced by foreign keys with ON DELETE "
|
||||
"actions that would fire when the old table is dropped: "
|
||||
"{fks}. Call transform() outside of the transaction, or "
|
||||
'execute "PRAGMA foreign_keys = off" before opening it.'.format(
|
||||
table=self.name,
|
||||
fks=", ".join(
|
||||
"{}.{} (ON DELETE {})".format(
|
||||
table_name, ", ".join(fk.columns), fk.on_delete
|
||||
)
|
||||
for table_name, fk in destructive_fks
|
||||
),
|
||||
)
|
||||
)
|
||||
defer_foreign_keys_was_on = False
|
||||
try:
|
||||
if should_disable_foreign_keys:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue