Skip to content

Commit ffe2213

Browse files
committed
Add investigation of FK constraints during transform()
Document findings from investigation into how the transform() method handles foreign key constraints that REFERENCE the table being transformed (incoming FKs from other tables). Key findings: - Incoming FKs survive transform because SQLite's ALTER TABLE RENAME automatically updates FK references in the schema - Renaming a referenced COLUMN breaks incoming FKs because SQLite cannot update column references - With FK enforcement ON, transform() correctly detects and rolls back operations that would break FK constraints - With FK enforcement OFF, transforms that break FKs succeed but leave the database in an inconsistent state Also documents a minor issue: leftover temp tables when transform fails.
1 parent 8d74ffc commit ffe2213

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

TRANSFORM_FK_INVESTIGATION.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Investigation: Foreign Key Constraints During transform()
2+
3+
This document summarizes the investigation into how the `transform()` method handles
4+
foreign key constraints that **reference** the table being transformed (incoming FKs).
5+
6+
## Background
7+
8+
The `transform()` method in sqlite-utils performs the following steps:
9+
1. Create a new temporary table with the desired schema
10+
2. Copy data from the old table to the new table
11+
3. Drop the old table
12+
4. Rename the new table to the original table name
13+
5. Recreate indexes
14+
15+
This raises the question: what happens to foreign key constraints in **other tables**
16+
that reference the table being transformed?
17+
18+
## Key Finding: SQLite's ALTER TABLE RENAME Behavior
19+
20+
SQLite's `ALTER TABLE ... RENAME TO` command automatically updates all foreign key
21+
references in the schema. This is documented in the SQLite documentation:
22+
23+
> "The RENAME command renames the table, and also updates all references to the
24+
> table within the schema" - https://www.sqlite.org/lang_altertable.html
25+
26+
This means:
27+
- When `authors_new_xxx` is renamed to `authors`, all FK constraints that reference
28+
`authors` continue to work correctly
29+
- The FK references are stored by table **name**, and the rename updates them
30+
31+
## Test Scenarios and Results
32+
33+
### Scenario 1: Simple transform (no column rename)
34+
```
35+
Setup: books.author_id REFERENCES authors(id)
36+
Action: db["authors"].transform(types={"name": str})
37+
Result: FK constraints survive intact (both with FK ON and OFF)
38+
```
39+
40+
### Scenario 2: Rename a non-referenced column
41+
```
42+
Setup: books.author_id REFERENCES authors(id)
43+
Action: db["authors"].transform(rename={"name": "author_name"})
44+
Result: FK constraints survive intact (both with FK ON and OFF)
45+
```
46+
47+
### Scenario 3: Rename the referenced column (FK ON)
48+
```
49+
Setup: books.author_id REFERENCES authors(id)
50+
Action: db["authors"].transform(rename={"id": "author_pk"})
51+
Result: FAILS - "foreign key mismatch" error, transaction rolled back
52+
```
53+
The transform correctly detects the FK violation via `PRAGMA foreign_key_check`
54+
and rolls back the transaction, preserving the original schema.
55+
56+
### Scenario 4: Rename the referenced column (FK OFF)
57+
```
58+
Setup: books.author_id REFERENCES authors(id)
59+
Action: db["authors"].transform(rename={"id": "author_pk"})
60+
Result: Transform succeeds, but FK constraint is now BROKEN
61+
```
62+
The FK in `books` still references `authors(id)` but that column no longer exists.
63+
Running `PRAGMA foreign_key_check` produces a "foreign key mismatch" error.
64+
65+
### Scenario 5: Self-referential FK
66+
```
67+
Setup: employees.manager_id REFERENCES employees(id)
68+
Action: db["employees"].transform(types={"name": str})
69+
Result: FK constraint survives intact
70+
```
71+
72+
### Scenario 6: Multiple tables referencing the transformed table
73+
```
74+
Setup: books.author_id, articles.writer_id, quotes.speaker_id all REFERENCE authors(id)
75+
Action: db["authors"].transform(types={"name": str})
76+
Result: All FK constraints survive intact
77+
```
78+
79+
## How transform() Ensures FK Safety
80+
81+
The `transform()` method (db.py lines 1853-1917) implements the following safety measures:
82+
83+
1. **Saves FK enforcement state**: Checks `PRAGMA foreign_keys` before starting
84+
2. **Disables FK enforcement**: Sets `PRAGMA foreign_keys=0` during the transform
85+
3. **Executes transform SQL**: Within a transaction (`with self.db.conn:`)
86+
4. **Validates FK integrity**: Runs `PRAGMA foreign_key_check` after the transform
87+
5. **Rolls back on failure**: If FK check fails, the transaction is rolled back
88+
6. **Restores FK state**: Re-enables FK enforcement if it was originally on
89+
90+
## Summary Table
91+
92+
| Scenario | FK ON | FK OFF |
93+
|-------------------------------------|--------------------|--------------------|
94+
| Simple transform | Works, FKs intact | Works, FKs intact |
95+
| Rename non-referenced column | Works, FKs intact | Works, FKs intact |
96+
| Rename referenced column | FAILS (rollback) | Works, FKs BROKEN! |
97+
| Drop referenced column | FAILS (rollback) | Works, FKs BROKEN! |
98+
| Self-referential FK | Works, FKs intact | Works, FKs intact |
99+
| Multiple tables with FKs | Works, FKs intact | Works, FKs intact |
100+
101+
## Known Issue: Leftover Temp Table on Failure
102+
103+
When `transform()` fails (e.g., due to FK check failure), there may be a leftover
104+
temporary table (e.g., `authors_new_xxx`). This appears to be because the error
105+
occurs after some statements have executed but before the transaction fully commits.
106+
107+
The original table remains intact, so this is a minor cosmetic issue rather than
108+
a data integrity problem.
109+
110+
## Recommendations
111+
112+
1. **Always use FK enforcement** (`PRAGMA foreign_keys=ON`) when working with
113+
relational data to ensure transform() catches FK violations early
114+
115+
2. **Be cautious when renaming columns**: If a column is referenced by FKs from
116+
other tables, you'll need to update those FKs as well. Consider:
117+
- First transforming the referencing tables to update their FK constraints
118+
- Then transforming the referenced table to rename the column
119+
120+
3. **Use `foreign_key_check`** after bulk operations with FK enforcement off to
121+
verify data integrity
122+
123+
## Code References
124+
125+
- `transform()` method: sqlite_utils/db.py:1853-1917
126+
- `transform_sql()` method: sqlite_utils/db.py:1919-2127
127+
- FK handling in transform_sql: sqlite_utils/db.py:1957-1993
128+
- Related tests: tests/test_transform.py:301-500

0 commit comments

Comments
 (0)