Add cookbook recipe for adding a non-nullable column - #1838
Add cookbook recipe for adding a non-nullable column#1838ArockiaRajamanickam wants to merge 2 commits into
Conversation
Documents the add-nullable, backfill, then set NOT NULL pattern agreed on in issue sqlalchemy#681, along with the server_default alternative and a warning about table locking on a running application.
MohammedAlkindi
left a comment
There was a problem hiding this comment.
I executed the recipe rather than just reading it — docs that ship runnable steps deserve a runnable review. Windows 11, CPython 3.13.13, SQLAlchemy 2.x, in-memory SQLite, table pre-populated with two rows.
What checks out, step by step:
- The opening claim reproduces:
op.add_column(..., nullable=False)on the populated table is rejected (OperationalError, "Cannot add a NOT NULL column with default value NULL" class of failure). - Step 1 (nullable add) and step 2 (backfill through the lightweight
sa.table()/sa.column()constructs) run exactly as written, and the backfill lands: both pre-existing rows readstatus='active'. - After the constraint is applied, a
NULLinsert is rejected withIntegrityError— the end state is what the recipe promises.
One real gap: step 3 as written fails on SQLite.
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "ALTER": syntax error
[SQL: ALTER TABLE account ALTER COLUMN status SET NOT NULL]
SQLite has no ALTER COLUMN, so op.alter_column(...) emits SQL it cannot execute. The same three steps succeed if step 3 goes through batch mode — I verified this end-to-end (recipe completes, backfill correct, NULL insert rejected):
with op.batch_alter_table("account") as batch_op:
batch_op.alter_column("status", existing_type=sa.String(50), nullable=False)Since batch mode is this project's own canonical answer to SQLite's ALTER limitations (and works unchanged on the other backends), a short note — or making batch the primary form with a line saying plain alter_column suffices on PostgreSQL/MySQL — would keep the cookbook honest for the one backend where the current text throws. Given how many people trial migrations against SQLite before pointing them at production, I'd guess this exact error is the first thing a fair fraction of readers would hit.
The lock-duration warning at the end is a good inclusion and matches what the operation actually does.
Step 3 emits ALTER TABLE ... ALTER COLUMN, which SQLite rejects as a syntax error on the versions most people are running, so the recipe as written failed on the backend readers are most likely to try it against. The batch form is now shown next to step 3 rather than only referenced by a seealso at the end of the section. Reported by Mohammed Alkindi in review of sqlalchemy#1838.
|
Thank you for actually running it, that caught a real problem. You are right, and I owe you a correction on the detail. When I wrote this I checked
So the syntax arrived somewhere in 3.52/3.53, which is recent enough that whether the recipe works depends on the SQLite your Python happens to be linked against. My machine picks up 3.53, yours does not, and yours is the situation nearly every reader is in today. A Fixed in 4f553f8. The batch form now appears directly after step 3, with the actual error text so people can match it, plus a line noting the batch form works on the other backends too and can be used unconditionally. I verified the batch version end to end the way you did: backfill lands on both rows, the resulting schema carries |
|
Confirmed 4f553f8 resolves what I flagged, on a stock SQLite. Ran the updated recipe steps against SQLite 3.50.4 (alembic 1.19.1, SQLAlchemy 2.0.52, Python 3.13, Windows):
3.50.4 is what a current CPython install links against, which supports the placement call: the batch form next to step 3, rather than in a |
|
Thanks for re-running it, and the 3.50.4 number is the useful part. That is the piece I could not establish from here. My machines link 3.53, so I could only bracket the change between 3.51 (syntax error) and 3.53 (accepted and enforced). Your 3.50.4 being what a current CPython install ships confirms the practical point: for the foreseeable future almost every reader trying this against SQLite is on a version where the plain form throws, which is exactly why it needed to sit next to step 3 rather than in a Appreciate you checking the quoted error matches character for character. That was deliberate, so someone who hits it can search the page for what their traceback says. |
Adds the cookbook recipe requested in the second bullet of #681: how to add a non-nullable column to a table that already has rows, without a server default.
The recipe follows what was agreed in the thread rather than inventing an approach:
UPDATE, thenalter_column(nullable=False)), which was the one both of you settled on... warning::covers zzzeek's two points: theUPDATE/ALTERcan lock the table and take a live site down on a large dataset, and adding a non-nullable column is hard to make work against an application that keeps running during the migration.server_defaultalternative is mentioned second, with the note that dropping a server default behaves differently across backends.:ref:batch_migrations`` instead.The backfill uses the lightweight
sa.table()/sa.column()constructs rather than the application model, so the migration keeps working as the model changes.Scope: this PR only covers the cookbook half of #681. The
env.pylogging half was declined by zzzeek in the same thread, so it is left alone, and I have not usedFixes:since the issue bundles both requests.Verification:
MigrationContextagainst SQLite 3.53 and PostgreSQL 16: confirmed the naive one-stepadd_column(nullable=False)fails on a populated table, that the three-step recipe leaves an enforcedNOT NULLcolumn, that a subsequentNULLinsert is rejected, that the downgrade drops the column, and that theserver_defaultvariant leaves no residual default.sphinx -b htmlbuilds with no new warnings; the:ref:and:meth:targets and the two SQLAlchemy intersphinx links all resolve.One note on the SQLite cross-reference: the
seealsosays thenullable=Falsestep may need batch mode on backends with limitedALTERsupport, rather than stating flatly that SQLite cannot do it. While testing, SQLite 3.53 acceptedALTER TABLE ... ALTER COLUMN ... SET NOT NULLdirectly, so the absolute phrasing would not have been accurate on current versions. Happy to reword if you would rather it match the framing inbatch.rst.I used an AI assistant while drafting this; the wording and the testing above are mine and I have checked the content against the thread and the docs build.