[{"data":1,"prerenderedAt":3647},["ShallowReactive",2],{"page-\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002F":3},{"id":4,"title":5,"body":6,"description":3639,"extension":3640,"meta":3641,"navigation":248,"path":3643,"seo":3644,"stem":3645,"__hash__":3646},"content\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Findex.md","Zero-Downtime Schema Migration Strategies with Alembic and PostgreSQL",{"type":7,"value":8,"toc":3615},"minimark",[9,13,23,28,40,61,64,87,90,203,206,210,215,226,546,567,590,594,608,618,896,914,921,1135,1159,1163,1167,1181,1482,1500,1504,1528,1535,1539,1543,1546,1564,1567,1907,1918,1922,1932,1952,1959,1963,1971,2195,2210,2214,2225,2551,2561,2565,2569,2583,2975,2979,3012,3030,3068,3076,3080,3207,3211,3216,3238,3246,3268,3276,3291,3300,3303,3501,3509,3524,3529,3553,3557,3611],[10,11,5],"h1",{"id":12},"zero-downtime-schema-migration-strategies-with-alembic-and-postgresql",[14,15,16,17,22],"p",{},"Running schema changes against a live PostgreSQL database without downtime requires deliberate sequencing, careful lock management, and a disciplined split between schema evolution and application deployment. The core principle is that your database schema and your application code must remain mutually compatible across at least two consecutive releases — the current version and the one being rolled out. This guide covers the expand-contract pattern, safe column additions, concurrent index builds, lock timeout guards, and how to orchestrate destructive changes across separate deployments using ",[18,19,21],"a",{"href":20},"\u002Falembic-async-migrations-and-schema-evolution\u002F","Alembic with async SQLAlchemy engines",".",[24,25,27],"h2",{"id":26},"concept-execution-model","Concept & Execution Model",[14,29,30,31,35,36,39],{},"Zero-downtime migrations rest on a single invariant: at no point during a rolling deployment should the running application version be unable to read or write the database. This rules out any migration that removes a column before all app instances stop using it, adds a ",[32,33,34],"code",{},"NOT NULL"," constraint before backfilling every row, or holds an ",[32,37,38],{},"AccessExclusiveLock"," long enough to queue writes behind it.",[14,41,42,43,46,47,46,50,53,54,56,57,60],{},"PostgreSQL's locking model is the primary obstacle. DDL statements — ",[32,44,45],{},"ALTER TABLE",", ",[32,48,49],{},"DROP COLUMN",[32,51,52],{},"ADD CONSTRAINT"," — acquire an ",[32,55,38],{}," on the target table that blocks every concurrent read and write for the duration of the statement. On large tables, a single unconstrained ",[32,58,59],{},"ALTER TABLE ADD COLUMN DEFAULT 'x'"," could rewrite millions of rows under lock, stalling production traffic for seconds or minutes. PostgreSQL 11 eliminated the rewrite for simple defaults, but constraints, indexes, and nullable changes still demand care.",[14,62,63],{},"The expand-contract pattern structures schema evolution into three distinct phases that map cleanly to release boundaries:",[65,66,67,75,81],"ul",{},[68,69,70,74],"li",{},[71,72,73],"strong",{},"Expand"," — add the new column, table, or index alongside the existing structure. Old code continues to work unchanged; new code can optionally use the new structure.",[68,76,77,80],{},[71,78,79],{},"Migrate"," — backfill data, create supporting indexes, and validate constraints. The database and all running app versions remain compatible.",[68,82,83,86],{},[71,84,85],{},"Contract"," — remove the now-obsolete column, table, or constraint after all application instances have been updated and verified.",[14,88,89],{},"Each phase ships as one or more Alembic migration scripts, each targeting a specific release. The SVG below illustrates the release timeline:",[91,92,97,98,97,102,97,97,106,97,97,114,97,97,122,97,127,97,134,97,138,97,97,144,97,151,97,97,157,97,162,97,166,97,168,97,97,171,97,174,97,97,177,97,180,97,184,97,186,97,97,189,97,97,193,97,197,97,200],"svg",{"viewBox":93,"role":94,"ariaLabel":95,"xmlns":96},"0 0 780 200","img","Expand-migrate-contract release timeline across three phases","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[99,100,101],"title",{},"Expand–Migrate–Contract Release Timeline",[103,104,105],"desc",{},"Three horizontal phases across a timeline: Phase 1 Expand (add new column), Phase 2 Migrate (backfill and index), Phase 3 Contract (drop old column). Release boundaries separate the phases.",[107,108],"rect",{"x":109,"y":109,"width":110,"height":111,"rx":112,"fill":113},"0","780","200","8","#f1f9f6",[115,116],"line",{"x1":117,"y1":118,"x2":119,"y2":118,"stroke":120,"style":121},"30","105","750","#0f766e","stroke-width:2.5",[107,123],{"x":117,"y":124,"width":111,"height":125,"rx":126,"fill":120},"65","60","6",[128,129,133],"text",{"x":130,"y":131,"fill":113,"style":132},"130","90","text-anchor:middle;font-size:13px;font-weight:bold","Phase 1",[128,135,73],{"x":130,"y":136,"fill":113,"style":137},"108","text-anchor:middle;font-size:11px",[128,139,143],{"x":130,"y":140,"fill":141,"style":142},"124","#d1ede8","text-anchor:middle;font-size:10px","Add column \u002F table",[115,145],{"x1":146,"y1":147,"x2":146,"y2":148,"stroke":149,"style":150},"240","50","160","#113f39","stroke-width:1.5;stroke-dasharray:5,3",[128,152,156],{"x":146,"y":153,"fill":154,"style":155},"46","#3f4f4b","text-anchor:middle;font-size:9px","Release N+1",[107,158],{"x":159,"y":124,"width":160,"height":125,"rx":126,"fill":161},"250","220","#1f9f95",[128,163,165],{"x":164,"y":131,"fill":113,"style":132},"360","Phase 2",[128,167,79],{"x":164,"y":136,"fill":113,"style":137},[128,169,170],{"x":164,"y":140,"fill":141,"style":142},"Backfill · Index · Validate",[115,172],{"x1":173,"y1":147,"x2":173,"y2":148,"stroke":149,"style":150},"480",[128,175,176],{"x":173,"y":153,"fill":154,"style":155},"Release N+2",[107,178],{"x":179,"y":124,"width":160,"height":125,"rx":126,"fill":149},"490",[128,181,183],{"x":182,"y":131,"fill":113,"style":132},"600","Phase 3",[128,185,85],{"x":182,"y":136,"fill":113,"style":137},[128,187,188],{"x":182,"y":140,"fill":141,"style":142},"Drop old column\u002Fconstraint",[190,191],"polygon",{"points":192,"fill":120},"750,100 762,105 750,110",[128,194,196],{"x":130,"y":195,"fill":154,"style":155},"175","Old code ✓  New code ✓",[128,198,199],{"x":164,"y":195,"fill":154,"style":155},"Both versions compatible",[128,201,202],{"x":182,"y":195,"fill":154,"style":155},"Old code removed",[14,204,205],{},"Importantly, the expand and contract phases must ship in separate releases separated by enough time to confirm a full fleet rollout. Collapsing them into a single deployment is the most common source of production incidents.",[24,207,209],{"id":208},"migration-construction-alembic-execution-patterns","Migration Construction & Alembic Execution Patterns",[211,212,214],"h3",{"id":213},"adding-columns-with-defaults-safely","Adding Columns with Defaults Safely",[14,216,217,218,221,222,225],{},"PostgreSQL 11 introduced a major optimisation: adding a column with a ",[32,219,220],{},"DEFAULT"," value no longer rewrites the table. Instead, PostgreSQL stores the default in ",[32,223,224],{},"pg_attribute"," and serves it lazily for existing rows. This makes the following pattern safe even on tables with hundreds of millions of rows:",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-python shiki shiki-themes github-light github-dark","\"\"\"Add status column to orders with a safe server default.\n\nRevision ID: a1b2c3d4e5f6\nRevises: 9z8y7x6w5v4u\nCreate Date: 2026-06-18 09:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision = \"a1b2c3d4e5f6\"\ndown_revision = \"9z8y7x6w5v4u\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    # PostgreSQL 11+: server_default avoids a full table rewrite.\n    # Use server_default (DDL-level) NOT default (Python-level) for this guarantee.\n    op.add_column(\n        \"orders\",\n        sa.Column(\n            \"fulfillment_status\",\n            sa.String(length=32),\n            nullable=True,           # Nullable in the expand phase; NOT NULL comes later\n            server_default=\"pending\",\n        ),\n    )\n\n\ndef downgrade() -> None:\n    op.drop_column(\"orders\", \"fulfillment_status\")\n","python","",[32,234,235,243,250,256,262,268,274,292,297,311,325,330,342,353,364,374,379,384,403,410,416,422,431,437,445,463,480,493,499,505,510,515,529],{"__ignoreMap":232},[236,237,239],"span",{"class":115,"line":238},1,[236,240,242],{"class":241},"sZZnC","\"\"\"Add status column to orders with a safe server default.\n",[236,244,246],{"class":115,"line":245},2,[236,247,249],{"emptyLinePlaceholder":248},true,"\n",[236,251,253],{"class":115,"line":252},3,[236,254,255],{"class":241},"Revision ID: a1b2c3d4e5f6\n",[236,257,259],{"class":115,"line":258},4,[236,260,261],{"class":241},"Revises: 9z8y7x6w5v4u\n",[236,263,265],{"class":115,"line":264},5,[236,266,267],{"class":241},"Create Date: 2026-06-18 09:00:00.000000\n",[236,269,271],{"class":115,"line":270},6,[236,272,273],{"class":241},"\"\"\"\n",[236,275,277,281,285,288],{"class":115,"line":276},7,[236,278,280],{"class":279},"szBVR","from",[236,282,284],{"class":283},"sj4cs"," __future__",[236,286,287],{"class":279}," import",[236,289,291],{"class":290},"sVt8B"," annotations\n",[236,293,295],{"class":115,"line":294},8,[236,296,249],{"emptyLinePlaceholder":248},[236,298,300,302,305,308],{"class":115,"line":299},9,[236,301,280],{"class":279},[236,303,304],{"class":290}," alembic ",[236,306,307],{"class":279},"import",[236,309,310],{"class":290}," op\n",[236,312,314,316,319,322],{"class":115,"line":313},10,[236,315,307],{"class":279},[236,317,318],{"class":290}," sqlalchemy ",[236,320,321],{"class":279},"as",[236,323,324],{"class":290}," sa\n",[236,326,328],{"class":115,"line":327},11,[236,329,249],{"emptyLinePlaceholder":248},[236,331,333,336,339],{"class":115,"line":332},12,[236,334,335],{"class":290},"revision ",[236,337,338],{"class":279},"=",[236,340,341],{"class":241}," \"a1b2c3d4e5f6\"\n",[236,343,345,348,350],{"class":115,"line":344},13,[236,346,347],{"class":290},"down_revision ",[236,349,338],{"class":279},[236,351,352],{"class":241}," \"9z8y7x6w5v4u\"\n",[236,354,356,359,361],{"class":115,"line":355},14,[236,357,358],{"class":290},"branch_labels ",[236,360,338],{"class":279},[236,362,363],{"class":283}," None\n",[236,365,367,370,372],{"class":115,"line":366},15,[236,368,369],{"class":290},"depends_on ",[236,371,338],{"class":279},[236,373,363],{"class":283},[236,375,377],{"class":115,"line":376},16,[236,378,249],{"emptyLinePlaceholder":248},[236,380,382],{"class":115,"line":381},17,[236,383,249],{"emptyLinePlaceholder":248},[236,385,387,390,394,397,400],{"class":115,"line":386},18,[236,388,389],{"class":279},"def",[236,391,393],{"class":392},"sScJk"," upgrade",[236,395,396],{"class":290},"() -> ",[236,398,399],{"class":283},"None",[236,401,402],{"class":290},":\n",[236,404,406],{"class":115,"line":405},19,[236,407,409],{"class":408},"sJ8bj","    # PostgreSQL 11+: server_default avoids a full table rewrite.\n",[236,411,413],{"class":115,"line":412},20,[236,414,415],{"class":408},"    # Use server_default (DDL-level) NOT default (Python-level) for this guarantee.\n",[236,417,419],{"class":115,"line":418},21,[236,420,421],{"class":290},"    op.add_column(\n",[236,423,425,428],{"class":115,"line":424},22,[236,426,427],{"class":241},"        \"orders\"",[236,429,430],{"class":290},",\n",[236,432,434],{"class":115,"line":433},23,[236,435,436],{"class":290},"        sa.Column(\n",[236,438,440,443],{"class":115,"line":439},24,[236,441,442],{"class":241},"            \"fulfillment_status\"",[236,444,430],{"class":290},[236,446,448,451,455,457,460],{"class":115,"line":447},25,[236,449,450],{"class":290},"            sa.String(",[236,452,454],{"class":453},"s4XuR","length",[236,456,338],{"class":279},[236,458,459],{"class":283},"32",[236,461,462],{"class":290},"),\n",[236,464,466,469,471,474,477],{"class":115,"line":465},26,[236,467,468],{"class":453},"            nullable",[236,470,338],{"class":279},[236,472,473],{"class":283},"True",[236,475,476],{"class":290},",           ",[236,478,479],{"class":408},"# Nullable in the expand phase; NOT NULL comes later\n",[236,481,483,486,488,491],{"class":115,"line":482},27,[236,484,485],{"class":453},"            server_default",[236,487,338],{"class":279},[236,489,490],{"class":241},"\"pending\"",[236,492,430],{"class":290},[236,494,496],{"class":115,"line":495},28,[236,497,498],{"class":290},"        ),\n",[236,500,502],{"class":115,"line":501},29,[236,503,504],{"class":290},"    )\n",[236,506,508],{"class":115,"line":507},30,[236,509,249],{"emptyLinePlaceholder":248},[236,511,513],{"class":115,"line":512},31,[236,514,249],{"emptyLinePlaceholder":248},[236,516,518,520,523,525,527],{"class":115,"line":517},32,[236,519,389],{"class":279},[236,521,522],{"class":392}," downgrade",[236,524,396],{"class":290},[236,526,399],{"class":283},[236,528,402],{"class":290},[236,530,532,535,538,540,543],{"class":115,"line":531},33,[236,533,534],{"class":290},"    op.drop_column(",[236,536,537],{"class":241},"\"orders\"",[236,539,46],{"class":290},[236,541,542],{"class":241},"\"fulfillment_status\"",[236,544,545],{"class":290},")\n",[14,547,548,549,552,553,556,557,559,560,562,563,566],{},"The critical distinction is ",[32,550,551],{},"server_default"," versus ",[32,554,555],{},"default",". ",[32,558,551],{}," is a DDL-level expression evaluated by PostgreSQL itself, enabling the fast-path optimisation. ",[32,561,555],{}," is a Python-side value that SQLAlchemy inserts into every ",[32,564,565],{},"INSERT"," statement — it provides no DDL instruction and therefore triggers a full table rewrite for the column addition.",[14,568,569,570,572,573,575,576,578,579,582,583,585,586,22],{},"Avoid adding a ",[32,571,34],{}," column in the same migration as the column creation. Adding ",[32,574,34],{}," requires PostgreSQL to verify every row, which acquires an ",[32,577,38],{}," for the duration of the table scan. The correct sequence is: expand (nullable column with server_default), migrate (backfill, then add a ",[32,580,581],{},"CHECK NOT VALID"," constraint), contract (validate the constraint and alter to ",[32,584,34],{},"). The detailed walkthrough lives in ",[18,587,589],{"href":588},"\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Fadding-a-not-null-column-without-locking-in-postgres\u002F","adding a NOT NULL column without locking in Postgres",[211,591,593],{"id":592},"create-index-concurrently","CREATE INDEX CONCURRENTLY",[14,595,596,597,600,601,604,605,607],{},"Building an index on a large table without ",[32,598,599],{},"CONCURRENTLY"," acquires a ",[32,602,603],{},"ShareLock"," that blocks all writes for the index build duration — potentially minutes on large tables. ",[32,606,593],{}," performs the build in two passes without blocking writes, at the cost of taking longer overall.",[14,609,610,611,613,614,617],{},"The critical constraint: ",[32,612,593],{}," cannot run inside a transaction. Alembic wraps each migration in a transaction by default, so you must explicitly leave the transaction context. The correct approach uses ",[32,615,616],{},"op.get_bind()"," to retrieve the connection and issues the statement in autocommit mode via an explicit execution option:",[227,619,621],{"className":229,"code":620,"language":231,"meta":232,"style":232},"\"\"\"Create concurrent index on invoices.tenant_id.\n\nRevision ID: b2c3d4e5f6a7\nRevises: a1b2c3d4e5f6\nCreate Date: 2026-06-18 10:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n\nrevision = \"b2c3d4e5f6a7\"\ndown_revision = \"a1b2c3d4e5f6\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    # Disable the Alembic transaction wrapper for this migration.\n    # CREATE INDEX CONCURRENTLY cannot run inside a transaction block.\n    op.execute(sa.text(\"COMMIT\"))  # End the implicit transaction Alembic opened\n\n    # Execute in autocommit so PostgreSQL can run the concurrent build.\n    conn = op.get_bind()\n    conn.execution_options(isolation_level=\"AUTOCOMMIT\").execute(\n        sa.text(\n            \"CREATE INDEX CONCURRENTLY IF NOT EXISTS \"\n            \"ix_invoices_tenant_id ON invoices (tenant_id)\"\n        )\n    )\n\n\ndef downgrade() -> None:\n    conn = op.get_bind()\n    conn.execution_options(isolation_level=\"AUTOCOMMIT\").execute(\n        sa.text(\"DROP INDEX CONCURRENTLY IF EXISTS ix_invoices_tenant_id\")\n    )\n",[32,622,623,628,632,637,642,647,651,661,665,675,685,697,701,710,718,726,734,738,742,754,759,764,778,782,787,797,813,818,823,828,833,837,841,845,858,867,880,891],{"__ignoreMap":232},[236,624,625],{"class":115,"line":238},[236,626,627],{"class":241},"\"\"\"Create concurrent index on invoices.tenant_id.\n",[236,629,630],{"class":115,"line":245},[236,631,249],{"emptyLinePlaceholder":248},[236,633,634],{"class":115,"line":252},[236,635,636],{"class":241},"Revision ID: b2c3d4e5f6a7\n",[236,638,639],{"class":115,"line":258},[236,640,641],{"class":241},"Revises: a1b2c3d4e5f6\n",[236,643,644],{"class":115,"line":264},[236,645,646],{"class":241},"Create Date: 2026-06-18 10:00:00.000000\n",[236,648,649],{"class":115,"line":270},[236,650,273],{"class":241},[236,652,653,655,657,659],{"class":115,"line":276},[236,654,280],{"class":279},[236,656,284],{"class":283},[236,658,287],{"class":279},[236,660,291],{"class":290},[236,662,663],{"class":115,"line":294},[236,664,249],{"emptyLinePlaceholder":248},[236,666,667,669,671,673],{"class":115,"line":299},[236,668,280],{"class":279},[236,670,304],{"class":290},[236,672,307],{"class":279},[236,674,310],{"class":290},[236,676,677,679,681,683],{"class":115,"line":313},[236,678,307],{"class":279},[236,680,318],{"class":290},[236,682,321],{"class":279},[236,684,324],{"class":290},[236,686,687,689,692,694],{"class":115,"line":327},[236,688,280],{"class":279},[236,690,691],{"class":290}," sqlalchemy.dialects ",[236,693,307],{"class":279},[236,695,696],{"class":290}," postgresql\n",[236,698,699],{"class":115,"line":332},[236,700,249],{"emptyLinePlaceholder":248},[236,702,703,705,707],{"class":115,"line":344},[236,704,335],{"class":290},[236,706,338],{"class":279},[236,708,709],{"class":241}," \"b2c3d4e5f6a7\"\n",[236,711,712,714,716],{"class":115,"line":355},[236,713,347],{"class":290},[236,715,338],{"class":279},[236,717,341],{"class":241},[236,719,720,722,724],{"class":115,"line":366},[236,721,358],{"class":290},[236,723,338],{"class":279},[236,725,363],{"class":283},[236,727,728,730,732],{"class":115,"line":376},[236,729,369],{"class":290},[236,731,338],{"class":279},[236,733,363],{"class":283},[236,735,736],{"class":115,"line":381},[236,737,249],{"emptyLinePlaceholder":248},[236,739,740],{"class":115,"line":386},[236,741,249],{"emptyLinePlaceholder":248},[236,743,744,746,748,750,752],{"class":115,"line":405},[236,745,389],{"class":279},[236,747,393],{"class":392},[236,749,396],{"class":290},[236,751,399],{"class":283},[236,753,402],{"class":290},[236,755,756],{"class":115,"line":412},[236,757,758],{"class":408},"    # Disable the Alembic transaction wrapper for this migration.\n",[236,760,761],{"class":115,"line":418},[236,762,763],{"class":408},"    # CREATE INDEX CONCURRENTLY cannot run inside a transaction block.\n",[236,765,766,769,772,775],{"class":115,"line":424},[236,767,768],{"class":290},"    op.execute(sa.text(",[236,770,771],{"class":241},"\"COMMIT\"",[236,773,774],{"class":290},"))  ",[236,776,777],{"class":408},"# End the implicit transaction Alembic opened\n",[236,779,780],{"class":115,"line":433},[236,781,249],{"emptyLinePlaceholder":248},[236,783,784],{"class":115,"line":439},[236,785,786],{"class":408},"    # Execute in autocommit so PostgreSQL can run the concurrent build.\n",[236,788,789,792,794],{"class":115,"line":447},[236,790,791],{"class":290},"    conn ",[236,793,338],{"class":279},[236,795,796],{"class":290}," op.get_bind()\n",[236,798,799,802,805,807,810],{"class":115,"line":465},[236,800,801],{"class":290},"    conn.execution_options(",[236,803,804],{"class":453},"isolation_level",[236,806,338],{"class":279},[236,808,809],{"class":241},"\"AUTOCOMMIT\"",[236,811,812],{"class":290},").execute(\n",[236,814,815],{"class":115,"line":482},[236,816,817],{"class":290},"        sa.text(\n",[236,819,820],{"class":115,"line":495},[236,821,822],{"class":241},"            \"CREATE INDEX CONCURRENTLY IF NOT EXISTS \"\n",[236,824,825],{"class":115,"line":501},[236,826,827],{"class":241},"            \"ix_invoices_tenant_id ON invoices (tenant_id)\"\n",[236,829,830],{"class":115,"line":507},[236,831,832],{"class":290},"        )\n",[236,834,835],{"class":115,"line":512},[236,836,504],{"class":290},[236,838,839],{"class":115,"line":517},[236,840,249],{"emptyLinePlaceholder":248},[236,842,843],{"class":115,"line":531},[236,844,249],{"emptyLinePlaceholder":248},[236,846,848,850,852,854,856],{"class":115,"line":847},34,[236,849,389],{"class":279},[236,851,522],{"class":392},[236,853,396],{"class":290},[236,855,399],{"class":283},[236,857,402],{"class":290},[236,859,861,863,865],{"class":115,"line":860},35,[236,862,791],{"class":290},[236,864,338],{"class":279},[236,866,796],{"class":290},[236,868,870,872,874,876,878],{"class":115,"line":869},36,[236,871,801],{"class":290},[236,873,804],{"class":453},[236,875,338],{"class":279},[236,877,809],{"class":241},[236,879,812],{"class":290},[236,881,883,886,889],{"class":115,"line":882},37,[236,884,885],{"class":290},"        sa.text(",[236,887,888],{"class":241},"\"DROP INDEX CONCURRENTLY IF EXISTS ix_invoices_tenant_id\"",[236,890,545],{"class":290},[236,892,894],{"class":115,"line":893},38,[236,895,504],{"class":290},[14,897,898,899,902,903,906,907,910,911,913],{},"A common anti-pattern is wrapping the index build inside ",[32,900,901],{},"alembic.context.begin_transaction()"," or calling ",[32,904,905],{},"migration_context.connection.execute()"," while still inside the Alembic transaction context. Both approaches raise ",[32,908,909],{},"psycopg2.errors.ActiveSqlTransaction"," (or the asyncpg equivalent) because PostgreSQL rejects ",[32,912,593],{}," inside any open transaction block.",[14,915,916,917,920],{},"When using an async engine, the pattern adapts slightly. Because Alembic's ",[32,918,919],{},"run_async_migrations"," function already manages connection lifecycle, you should configure the migration script to run in non-transactional mode from the start for any migration containing concurrent index operations:",[227,922,924],{"className":229,"code":923,"language":231,"meta":232,"style":232},"\"\"\"Async-compatible concurrent index migration.\n\nRevision ID: c3d4e5f6a7b8\nRevises: b2c3d4e5f6a7\nCreate Date: 2026-06-18 11:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision = \"c3d4e5f6a7b8\"\ndown_revision = \"b2c3d4e5f6a7\"\nbranch_labels = None\ndepends_on = None\n\n# Tell Alembic not to wrap this migration in a transaction.\n# Required for CREATE INDEX CONCURRENTLY with asyncpg.\ntransactional_ddl = False  # Alembic respects this attribute\n\n\ndef upgrade() -> None:\n    op.execute(\n        sa.text(\n            \"CREATE INDEX CONCURRENTLY IF NOT EXISTS \"\n            \"ix_products_category_id ON products (category_id)\"\n        )\n    )\n\n\ndef downgrade() -> None:\n    op.execute(\n        sa.text(\"DROP INDEX CONCURRENTLY IF EXISTS ix_products_category_id\")\n    )\n",[32,925,926,931,935,940,945,950,954,964,968,978,988,992,1001,1009,1017,1025,1029,1034,1039,1052,1056,1060,1072,1077,1081,1085,1090,1094,1098,1102,1106,1118,1122,1131],{"__ignoreMap":232},[236,927,928],{"class":115,"line":238},[236,929,930],{"class":241},"\"\"\"Async-compatible concurrent index migration.\n",[236,932,933],{"class":115,"line":245},[236,934,249],{"emptyLinePlaceholder":248},[236,936,937],{"class":115,"line":252},[236,938,939],{"class":241},"Revision ID: c3d4e5f6a7b8\n",[236,941,942],{"class":115,"line":258},[236,943,944],{"class":241},"Revises: b2c3d4e5f6a7\n",[236,946,947],{"class":115,"line":264},[236,948,949],{"class":241},"Create Date: 2026-06-18 11:00:00.000000\n",[236,951,952],{"class":115,"line":270},[236,953,273],{"class":241},[236,955,956,958,960,962],{"class":115,"line":276},[236,957,280],{"class":279},[236,959,284],{"class":283},[236,961,287],{"class":279},[236,963,291],{"class":290},[236,965,966],{"class":115,"line":294},[236,967,249],{"emptyLinePlaceholder":248},[236,969,970,972,974,976],{"class":115,"line":299},[236,971,280],{"class":279},[236,973,304],{"class":290},[236,975,307],{"class":279},[236,977,310],{"class":290},[236,979,980,982,984,986],{"class":115,"line":313},[236,981,307],{"class":279},[236,983,318],{"class":290},[236,985,321],{"class":279},[236,987,324],{"class":290},[236,989,990],{"class":115,"line":327},[236,991,249],{"emptyLinePlaceholder":248},[236,993,994,996,998],{"class":115,"line":332},[236,995,335],{"class":290},[236,997,338],{"class":279},[236,999,1000],{"class":241}," \"c3d4e5f6a7b8\"\n",[236,1002,1003,1005,1007],{"class":115,"line":344},[236,1004,347],{"class":290},[236,1006,338],{"class":279},[236,1008,709],{"class":241},[236,1010,1011,1013,1015],{"class":115,"line":355},[236,1012,358],{"class":290},[236,1014,338],{"class":279},[236,1016,363],{"class":283},[236,1018,1019,1021,1023],{"class":115,"line":366},[236,1020,369],{"class":290},[236,1022,338],{"class":279},[236,1024,363],{"class":283},[236,1026,1027],{"class":115,"line":376},[236,1028,249],{"emptyLinePlaceholder":248},[236,1030,1031],{"class":115,"line":381},[236,1032,1033],{"class":408},"# Tell Alembic not to wrap this migration in a transaction.\n",[236,1035,1036],{"class":115,"line":386},[236,1037,1038],{"class":408},"# Required for CREATE INDEX CONCURRENTLY with asyncpg.\n",[236,1040,1041,1044,1046,1049],{"class":115,"line":405},[236,1042,1043],{"class":290},"transactional_ddl ",[236,1045,338],{"class":279},[236,1047,1048],{"class":283}," False",[236,1050,1051],{"class":408},"  # Alembic respects this attribute\n",[236,1053,1054],{"class":115,"line":412},[236,1055,249],{"emptyLinePlaceholder":248},[236,1057,1058],{"class":115,"line":418},[236,1059,249],{"emptyLinePlaceholder":248},[236,1061,1062,1064,1066,1068,1070],{"class":115,"line":424},[236,1063,389],{"class":279},[236,1065,393],{"class":392},[236,1067,396],{"class":290},[236,1069,399],{"class":283},[236,1071,402],{"class":290},[236,1073,1074],{"class":115,"line":433},[236,1075,1076],{"class":290},"    op.execute(\n",[236,1078,1079],{"class":115,"line":439},[236,1080,817],{"class":290},[236,1082,1083],{"class":115,"line":447},[236,1084,822],{"class":241},[236,1086,1087],{"class":115,"line":465},[236,1088,1089],{"class":241},"            \"ix_products_category_id ON products (category_id)\"\n",[236,1091,1092],{"class":115,"line":482},[236,1093,832],{"class":290},[236,1095,1096],{"class":115,"line":495},[236,1097,504],{"class":290},[236,1099,1100],{"class":115,"line":501},[236,1101,249],{"emptyLinePlaceholder":248},[236,1103,1104],{"class":115,"line":507},[236,1105,249],{"emptyLinePlaceholder":248},[236,1107,1108,1110,1112,1114,1116],{"class":115,"line":512},[236,1109,389],{"class":279},[236,1111,522],{"class":392},[236,1113,396],{"class":290},[236,1115,399],{"class":283},[236,1117,402],{"class":290},[236,1119,1120],{"class":115,"line":517},[236,1121,1076],{"class":290},[236,1123,1124,1126,1129],{"class":115,"line":531},[236,1125,885],{"class":290},[236,1127,1128],{"class":241},"\"DROP INDEX CONCURRENTLY IF EXISTS ix_products_category_id\"",[236,1130,545],{"class":290},[236,1132,1133],{"class":115,"line":847},[236,1134,504],{"class":290},[14,1136,1137,1138,1141,1142,1145,1146,1149,1150,1154,1155,1158],{},"Setting ",[32,1139,1140],{},"transactional_ddl = False"," at module level in the migration file instructs Alembic to skip the implicit ",[32,1143,1144],{},"BEGIN"," \u002F ",[32,1147,1148],{},"COMMIT"," wrapping, which is the cleanest path when the entire migration consists of concurrent DDL operations. See ",[18,1151,1153],{"href":1152},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002F","configuring Alembic with async SQLAlchemy engines"," for the ",[32,1156,1157],{},"env.py"," setup that wires async connections into the Alembic migration context.",[24,1160,1162],{"id":1161},"transaction-boundaries-lock-management","Transaction Boundaries & Lock Management",[211,1164,1166],{"id":1165},"setting-lock-timeouts","Setting Lock Timeouts",[14,1168,1169,1170,1172,1173,1176,1177,1180],{},"Long-running schema changes that queue behind an existing long-running query cause cascading problems: they hold an ",[32,1171,38],{}," while waiting, and every subsequent query to that table queues behind them. A 2-second ",[32,1174,1175],{},"lock_timeout"," combined with a ",[32,1178,1179],{},"statement_timeout"," cap is the minimal guard for any migration that touches a live table.",[227,1182,1184],{"className":229,"code":1183,"language":231,"meta":232,"style":232},"\"\"\"Migration with explicit lock and statement timeouts.\n\nRevision ID: d4e5f6a7b8c9\nRevises: c3d4e5f6a7b8\nCreate Date: 2026-06-18 12:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision = \"d4e5f6a7b8c9\"\ndown_revision = \"c3d4e5f6a7b8\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    # Fail fast rather than queue behind a long-running transaction.\n    # lock_timeout: abort if we cannot acquire the lock within 2 seconds.\n    # statement_timeout: abort the entire statement if it runs longer than 30s.\n    op.execute(sa.text(\"SET lock_timeout = '2s'\"))\n    op.execute(sa.text(\"SET statement_timeout = '30s'\"))\n\n    op.add_column(\n        \"tenants\",\n        sa.Column(\"plan_tier\", sa.String(length=16), nullable=True),\n    )\n\n    # Reset to session defaults so subsequent statements aren't constrained.\n    op.execute(sa.text(\"SET lock_timeout = DEFAULT\"))\n    op.execute(sa.text(\"SET statement_timeout = DEFAULT\"))\n\n\ndef downgrade() -> None:\n    op.execute(sa.text(\"SET lock_timeout = '2s'\"))\n    op.execute(sa.text(\"SET statement_timeout = '30s'\"))\n    op.drop_column(\"tenants\", \"plan_tier\")\n    op.execute(sa.text(\"SET lock_timeout = DEFAULT\"))\n    op.execute(sa.text(\"SET statement_timeout = DEFAULT\"))\n",[32,1185,1186,1191,1195,1200,1205,1210,1214,1224,1228,1238,1248,1252,1261,1269,1277,1285,1289,1293,1305,1310,1315,1320,1330,1339,1343,1347,1354,1384,1388,1392,1397,1406,1415,1419,1423,1435,1443,1451,1464,1473],{"__ignoreMap":232},[236,1187,1188],{"class":115,"line":238},[236,1189,1190],{"class":241},"\"\"\"Migration with explicit lock and statement timeouts.\n",[236,1192,1193],{"class":115,"line":245},[236,1194,249],{"emptyLinePlaceholder":248},[236,1196,1197],{"class":115,"line":252},[236,1198,1199],{"class":241},"Revision ID: d4e5f6a7b8c9\n",[236,1201,1202],{"class":115,"line":258},[236,1203,1204],{"class":241},"Revises: c3d4e5f6a7b8\n",[236,1206,1207],{"class":115,"line":264},[236,1208,1209],{"class":241},"Create Date: 2026-06-18 12:00:00.000000\n",[236,1211,1212],{"class":115,"line":270},[236,1213,273],{"class":241},[236,1215,1216,1218,1220,1222],{"class":115,"line":276},[236,1217,280],{"class":279},[236,1219,284],{"class":283},[236,1221,287],{"class":279},[236,1223,291],{"class":290},[236,1225,1226],{"class":115,"line":294},[236,1227,249],{"emptyLinePlaceholder":248},[236,1229,1230,1232,1234,1236],{"class":115,"line":299},[236,1231,280],{"class":279},[236,1233,304],{"class":290},[236,1235,307],{"class":279},[236,1237,310],{"class":290},[236,1239,1240,1242,1244,1246],{"class":115,"line":313},[236,1241,307],{"class":279},[236,1243,318],{"class":290},[236,1245,321],{"class":279},[236,1247,324],{"class":290},[236,1249,1250],{"class":115,"line":327},[236,1251,249],{"emptyLinePlaceholder":248},[236,1253,1254,1256,1258],{"class":115,"line":332},[236,1255,335],{"class":290},[236,1257,338],{"class":279},[236,1259,1260],{"class":241}," \"d4e5f6a7b8c9\"\n",[236,1262,1263,1265,1267],{"class":115,"line":344},[236,1264,347],{"class":290},[236,1266,338],{"class":279},[236,1268,1000],{"class":241},[236,1270,1271,1273,1275],{"class":115,"line":355},[236,1272,358],{"class":290},[236,1274,338],{"class":279},[236,1276,363],{"class":283},[236,1278,1279,1281,1283],{"class":115,"line":366},[236,1280,369],{"class":290},[236,1282,338],{"class":279},[236,1284,363],{"class":283},[236,1286,1287],{"class":115,"line":376},[236,1288,249],{"emptyLinePlaceholder":248},[236,1290,1291],{"class":115,"line":381},[236,1292,249],{"emptyLinePlaceholder":248},[236,1294,1295,1297,1299,1301,1303],{"class":115,"line":386},[236,1296,389],{"class":279},[236,1298,393],{"class":392},[236,1300,396],{"class":290},[236,1302,399],{"class":283},[236,1304,402],{"class":290},[236,1306,1307],{"class":115,"line":405},[236,1308,1309],{"class":408},"    # Fail fast rather than queue behind a long-running transaction.\n",[236,1311,1312],{"class":115,"line":412},[236,1313,1314],{"class":408},"    # lock_timeout: abort if we cannot acquire the lock within 2 seconds.\n",[236,1316,1317],{"class":115,"line":418},[236,1318,1319],{"class":408},"    # statement_timeout: abort the entire statement if it runs longer than 30s.\n",[236,1321,1322,1324,1327],{"class":115,"line":424},[236,1323,768],{"class":290},[236,1325,1326],{"class":241},"\"SET lock_timeout = '2s'\"",[236,1328,1329],{"class":290},"))\n",[236,1331,1332,1334,1337],{"class":115,"line":433},[236,1333,768],{"class":290},[236,1335,1336],{"class":241},"\"SET statement_timeout = '30s'\"",[236,1338,1329],{"class":290},[236,1340,1341],{"class":115,"line":439},[236,1342,249],{"emptyLinePlaceholder":248},[236,1344,1345],{"class":115,"line":447},[236,1346,421],{"class":290},[236,1348,1349,1352],{"class":115,"line":465},[236,1350,1351],{"class":241},"        \"tenants\"",[236,1353,430],{"class":290},[236,1355,1356,1359,1362,1365,1367,1369,1372,1375,1378,1380,1382],{"class":115,"line":482},[236,1357,1358],{"class":290},"        sa.Column(",[236,1360,1361],{"class":241},"\"plan_tier\"",[236,1363,1364],{"class":290},", sa.String(",[236,1366,454],{"class":453},[236,1368,338],{"class":279},[236,1370,1371],{"class":283},"16",[236,1373,1374],{"class":290},"), ",[236,1376,1377],{"class":453},"nullable",[236,1379,338],{"class":279},[236,1381,473],{"class":283},[236,1383,462],{"class":290},[236,1385,1386],{"class":115,"line":495},[236,1387,504],{"class":290},[236,1389,1390],{"class":115,"line":501},[236,1391,249],{"emptyLinePlaceholder":248},[236,1393,1394],{"class":115,"line":507},[236,1395,1396],{"class":408},"    # Reset to session defaults so subsequent statements aren't constrained.\n",[236,1398,1399,1401,1404],{"class":115,"line":512},[236,1400,768],{"class":290},[236,1402,1403],{"class":241},"\"SET lock_timeout = DEFAULT\"",[236,1405,1329],{"class":290},[236,1407,1408,1410,1413],{"class":115,"line":517},[236,1409,768],{"class":290},[236,1411,1412],{"class":241},"\"SET statement_timeout = DEFAULT\"",[236,1414,1329],{"class":290},[236,1416,1417],{"class":115,"line":531},[236,1418,249],{"emptyLinePlaceholder":248},[236,1420,1421],{"class":115,"line":847},[236,1422,249],{"emptyLinePlaceholder":248},[236,1424,1425,1427,1429,1431,1433],{"class":115,"line":860},[236,1426,389],{"class":279},[236,1428,522],{"class":392},[236,1430,396],{"class":290},[236,1432,399],{"class":283},[236,1434,402],{"class":290},[236,1436,1437,1439,1441],{"class":115,"line":869},[236,1438,768],{"class":290},[236,1440,1326],{"class":241},[236,1442,1329],{"class":290},[236,1444,1445,1447,1449],{"class":115,"line":882},[236,1446,768],{"class":290},[236,1448,1336],{"class":241},[236,1450,1329],{"class":290},[236,1452,1453,1455,1458,1460,1462],{"class":115,"line":893},[236,1454,534],{"class":290},[236,1456,1457],{"class":241},"\"tenants\"",[236,1459,46],{"class":290},[236,1461,1361],{"class":241},[236,1463,545],{"class":290},[236,1465,1467,1469,1471],{"class":115,"line":1466},39,[236,1468,768],{"class":290},[236,1470,1403],{"class":241},[236,1472,1329],{"class":290},[236,1474,1476,1478,1480],{"class":115,"line":1475},40,[236,1477,768],{"class":290},[236,1479,1412],{"class":241},[236,1481,1329],{"class":290},[14,1483,1484,1487,1488,1491,1492,1495,1496,1499],{},[32,1485,1486],{},"SET lock_timeout"," and ",[32,1489,1490],{},"SET statement_timeout"," are session-level settings that take effect immediately and persist only for the duration of the current connection. They do not require a DDL lock themselves. Setting them at the very start of ",[32,1493,1494],{},"upgrade()"," ensures that any subsequent DDL that cannot immediately acquire its lock will raise ",[32,1497,1498],{},"psycopg2.errors.LockNotAvailable"," within 2 seconds rather than building a queue.",[211,1501,1503],{"id":1502},"understanding-the-lock-queue","Understanding the Lock Queue",[14,1505,1506,1507,1510,1511,1513,1514,1516,1517,1520,1521,1524,1525,1527],{},"PostgreSQL's lock queue is FIFO. When a ",[32,1508,1509],{},"CREATE INDEX"," or ",[32,1512,45],{}," waits for an ",[32,1515,38],{},", every new query to that table queues behind it — even ",[32,1518,1519],{},"SELECT"," queries that would normally use ",[32,1522,1523],{},"AccessShareLock",". The combination of ",[32,1526,1175],{}," and scheduling migrations during low-traffic windows (combined with retry logic in your deployment pipeline) is the standard approach: if the migration times out, your deployment fails fast and cleanly rather than degrading production for minutes.",[14,1529,1530,1531,1534],{},"For the most demanding tables — millions of rows, high write concurrency, zero tolerance for lock waits — consider ",[32,1532,1533],{},"pg_try_advisory_lock"," to coordinate with application-level connection draining before running the DDL. This is an advanced pattern outside the scope of Alembic's built-in facilities.",[24,1536,1538],{"id":1537},"advanced-zero-downtime-patterns","Advanced Zero-Downtime Patterns",[211,1540,1542],{"id":1541},"backwards-compatible-deploys","Backwards-Compatible Deploys",[14,1544,1545],{},"Your application fleet runs multiple versions simultaneously during a rolling deployment. For a two-stage expand-contract cycle, this means:",[1547,1548,1549,1555,1558,1561],"ol",{},[68,1550,1551,1552,1554],{},"Release N ships the Alembic migration that adds the new column (nullable, with ",[32,1553,551],{},").",[68,1556,1557],{},"The new app code writes to both the old and new columns during the transition window.",[68,1559,1560],{},"Release N+1 removes the write to the old column and reads only from the new one.",[68,1562,1563],{},"Release N+2 ships the Alembic migration that drops the old column.",[14,1565,1566],{},"The \"write to both columns\" pattern in the transition release is sometimes called a dual-write. It is essential when the old column is still being read by concurrently deployed instances of the previous release.",[227,1568,1570],{"className":229,"code":1569,"language":231,"meta":232,"style":232},"from __future__ import annotations\n\nfrom sqlalchemy import String, select, update\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    # Legacy column — still present during transition\n    username: Mapped[str | None] = mapped_column(String(128), nullable=True)\n    # New canonical column — added in expand phase\n    display_name: Mapped[str | None] = mapped_column(String(128), nullable=True)\n\n\nasync def update_user_display(\n    session: AsyncSession, user_id: int, name: str\n) -> None:\n    \"\"\"Dual-write during transition: populate both columns simultaneously.\"\"\"\n    await session.execute(\n        update(User)\n        .where(User.id == user_id)\n        .values(\n            username=name,       # Keep populating for old app instances\n            display_name=name,   # Populate new column for new app instances\n        )\n    )\n    await session.commit()\n",[32,1571,1572,1582,1586,1597,1609,1621,1625,1629,1646,1651,1655,1659,1673,1683,1687,1715,1720,1754,1759,1788,1792,1796,1810,1823,1832,1837,1845,1850,1861,1866,1879,1892,1896,1900],{"__ignoreMap":232},[236,1573,1574,1576,1578,1580],{"class":115,"line":238},[236,1575,280],{"class":279},[236,1577,284],{"class":283},[236,1579,287],{"class":279},[236,1581,291],{"class":290},[236,1583,1584],{"class":115,"line":245},[236,1585,249],{"emptyLinePlaceholder":248},[236,1587,1588,1590,1592,1594],{"class":115,"line":252},[236,1589,280],{"class":279},[236,1591,318],{"class":290},[236,1593,307],{"class":279},[236,1595,1596],{"class":290}," String, select, update\n",[236,1598,1599,1601,1604,1606],{"class":115,"line":258},[236,1600,280],{"class":279},[236,1602,1603],{"class":290}," sqlalchemy.ext.asyncio ",[236,1605,307],{"class":279},[236,1607,1608],{"class":290}," AsyncSession\n",[236,1610,1611,1613,1616,1618],{"class":115,"line":264},[236,1612,280],{"class":279},[236,1614,1615],{"class":290}," sqlalchemy.orm ",[236,1617,307],{"class":279},[236,1619,1620],{"class":290}," Mapped, mapped_column, DeclarativeBase\n",[236,1622,1623],{"class":115,"line":270},[236,1624,249],{"emptyLinePlaceholder":248},[236,1626,1627],{"class":115,"line":276},[236,1628,249],{"emptyLinePlaceholder":248},[236,1630,1631,1634,1637,1640,1643],{"class":115,"line":294},[236,1632,1633],{"class":279},"class",[236,1635,1636],{"class":392}," Base",[236,1638,1639],{"class":290},"(",[236,1641,1642],{"class":392},"DeclarativeBase",[236,1644,1645],{"class":290},"):\n",[236,1647,1648],{"class":115,"line":299},[236,1649,1650],{"class":279},"    pass\n",[236,1652,1653],{"class":115,"line":313},[236,1654,249],{"emptyLinePlaceholder":248},[236,1656,1657],{"class":115,"line":327},[236,1658,249],{"emptyLinePlaceholder":248},[236,1660,1661,1663,1666,1668,1671],{"class":115,"line":332},[236,1662,1633],{"class":279},[236,1664,1665],{"class":392}," User",[236,1667,1639],{"class":290},[236,1669,1670],{"class":392},"Base",[236,1672,1645],{"class":290},[236,1674,1675,1678,1680],{"class":115,"line":344},[236,1676,1677],{"class":290},"    __tablename__ ",[236,1679,338],{"class":279},[236,1681,1682],{"class":241}," \"users\"\n",[236,1684,1685],{"class":115,"line":355},[236,1686,249],{"emptyLinePlaceholder":248},[236,1688,1689,1692,1695,1698,1701,1703,1706,1709,1711,1713],{"class":115,"line":366},[236,1690,1691],{"class":283},"    id",[236,1693,1694],{"class":290},": Mapped[",[236,1696,1697],{"class":283},"int",[236,1699,1700],{"class":290},"] ",[236,1702,338],{"class":279},[236,1704,1705],{"class":290}," mapped_column(",[236,1707,1708],{"class":453},"primary_key",[236,1710,338],{"class":279},[236,1712,473],{"class":283},[236,1714,545],{"class":290},[236,1716,1717],{"class":115,"line":376},[236,1718,1719],{"class":408},"    # Legacy column — still present during transition\n",[236,1721,1722,1725,1728,1731,1734,1736,1738,1741,1744,1746,1748,1750,1752],{"class":115,"line":381},[236,1723,1724],{"class":290},"    username: Mapped[",[236,1726,1727],{"class":283},"str",[236,1729,1730],{"class":279}," |",[236,1732,1733],{"class":283}," None",[236,1735,1700],{"class":290},[236,1737,338],{"class":279},[236,1739,1740],{"class":290}," mapped_column(String(",[236,1742,1743],{"class":283},"128",[236,1745,1374],{"class":290},[236,1747,1377],{"class":453},[236,1749,338],{"class":279},[236,1751,473],{"class":283},[236,1753,545],{"class":290},[236,1755,1756],{"class":115,"line":386},[236,1757,1758],{"class":408},"    # New canonical column — added in expand phase\n",[236,1760,1761,1764,1766,1768,1770,1772,1774,1776,1778,1780,1782,1784,1786],{"class":115,"line":405},[236,1762,1763],{"class":290},"    display_name: Mapped[",[236,1765,1727],{"class":283},[236,1767,1730],{"class":279},[236,1769,1733],{"class":283},[236,1771,1700],{"class":290},[236,1773,338],{"class":279},[236,1775,1740],{"class":290},[236,1777,1743],{"class":283},[236,1779,1374],{"class":290},[236,1781,1377],{"class":453},[236,1783,338],{"class":279},[236,1785,473],{"class":283},[236,1787,545],{"class":290},[236,1789,1790],{"class":115,"line":412},[236,1791,249],{"emptyLinePlaceholder":248},[236,1793,1794],{"class":115,"line":418},[236,1795,249],{"emptyLinePlaceholder":248},[236,1797,1798,1801,1804,1807],{"class":115,"line":424},[236,1799,1800],{"class":279},"async",[236,1802,1803],{"class":279}," def",[236,1805,1806],{"class":392}," update_user_display",[236,1808,1809],{"class":290},"(\n",[236,1811,1812,1815,1817,1820],{"class":115,"line":433},[236,1813,1814],{"class":290},"    session: AsyncSession, user_id: ",[236,1816,1697],{"class":283},[236,1818,1819],{"class":290},", name: ",[236,1821,1822],{"class":283},"str\n",[236,1824,1825,1828,1830],{"class":115,"line":439},[236,1826,1827],{"class":290},") -> ",[236,1829,399],{"class":283},[236,1831,402],{"class":290},[236,1833,1834],{"class":115,"line":447},[236,1835,1836],{"class":241},"    \"\"\"Dual-write during transition: populate both columns simultaneously.\"\"\"\n",[236,1838,1839,1842],{"class":115,"line":465},[236,1840,1841],{"class":279},"    await",[236,1843,1844],{"class":290}," session.execute(\n",[236,1846,1847],{"class":115,"line":482},[236,1848,1849],{"class":290},"        update(User)\n",[236,1851,1852,1855,1858],{"class":115,"line":495},[236,1853,1854],{"class":290},"        .where(User.id ",[236,1856,1857],{"class":279},"==",[236,1859,1860],{"class":290}," user_id)\n",[236,1862,1863],{"class":115,"line":501},[236,1864,1865],{"class":290},"        .values(\n",[236,1867,1868,1871,1873,1876],{"class":115,"line":507},[236,1869,1870],{"class":453},"            username",[236,1872,338],{"class":279},[236,1874,1875],{"class":290},"name,       ",[236,1877,1878],{"class":408},"# Keep populating for old app instances\n",[236,1880,1881,1884,1886,1889],{"class":115,"line":512},[236,1882,1883],{"class":453},"            display_name",[236,1885,338],{"class":279},[236,1887,1888],{"class":290},"name,   ",[236,1890,1891],{"class":408},"# Populate new column for new app instances\n",[236,1893,1894],{"class":115,"line":517},[236,1895,832],{"class":290},[236,1897,1898],{"class":115,"line":531},[236,1899,504],{"class":290},[236,1901,1902,1904],{"class":115,"line":847},[236,1903,1841],{"class":279},[236,1905,1906],{"class":290}," session.commit()\n",[14,1908,1909,1910,1913,1914,1917],{},"Once Release N+1 is fully deployed and verified, the dual-write simplifies to ",[32,1911,1912],{},"display_name"," only, and the ",[32,1915,1916],{},"username"," column is safe to drop in Release N+2.",[211,1919,1921],{"id":1920},"renaming-a-column","Renaming a Column",[14,1923,1924,1925,1928,1929,1931],{},"PostgreSQL supports ",[32,1926,1927],{},"ALTER TABLE RENAME COLUMN"," which acquires ",[32,1930,38],{}," only briefly (it updates catalog metadata, not rows). However, the application must not reference the old column name after the rename. The safe rename sequence is:",[1547,1933,1934,1937,1940,1943],{},[68,1935,1936],{},"Expand: add the new column, dual-write to both.",[68,1938,1939],{},"Migrate: backfill the new column from the old.",[68,1941,1942],{},"Contract release 1: remove reads of the old column from application code.",[68,1944,1945,1946,1510,1949,22],{},"Contract release 2: ",[32,1947,1948],{},"ALTER TABLE RENAME COLUMN old TO new",[32,1950,1951],{},"DROP COLUMN old",[14,1953,1954,1955,1958],{},"Avoid using Alembic's ",[32,1956,1957],{},"op.alter_column(..., new_column_name=...)"," on tables that are actively read in production without the dual-write transition in place.",[211,1960,1962],{"id":1961},"splitting-destructive-changes-across-releases","Splitting Destructive Changes Across Releases",[14,1964,1965,1966,1970],{},"Dropping a column is the canonical example of a destructive change. The rule is absolute: the Alembic migration that drops a column must ship in a separate release ",[1967,1968,1969],"em",{},"after"," all application code that reads or writes that column has been removed and deployed across the entire fleet.",[227,1972,1974],{"className":229,"code":1973,"language":231,"meta":232,"style":232},"\"\"\"PHASE 3 ONLY: Drop legacy username column after full fleet migration.\n\nThis migration MUST NOT be merged until Release N+1 (which removes all\napplication references to 'username') is fully deployed and verified.\n\nRevision ID: e5f6a7b8c9d0\nRevises: d4e5f6a7b8c9\nCreate Date: 2026-06-18 14:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision = \"e5f6a7b8c9d0\"\ndown_revision = \"d4e5f6a7b8c9\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    op.execute(sa.text(\"SET lock_timeout = '2s'\"))\n    op.drop_column(\"users\", \"username\")\n\n\ndef downgrade() -> None:\n    # Restore the column (data loss — downgrade restores structure only).\n    op.add_column(\n        \"users\",\n        sa.Column(\"username\", sa.String(length=128), nullable=True),\n    )\n",[32,1975,1976,1981,1985,1990,1995,1999,2004,2009,2014,2018,2028,2032,2042,2052,2056,2065,2073,2081,2089,2093,2097,2109,2117,2131,2135,2139,2151,2156,2160,2167,2191],{"__ignoreMap":232},[236,1977,1978],{"class":115,"line":238},[236,1979,1980],{"class":241},"\"\"\"PHASE 3 ONLY: Drop legacy username column after full fleet migration.\n",[236,1982,1983],{"class":115,"line":245},[236,1984,249],{"emptyLinePlaceholder":248},[236,1986,1987],{"class":115,"line":252},[236,1988,1989],{"class":241},"This migration MUST NOT be merged until Release N+1 (which removes all\n",[236,1991,1992],{"class":115,"line":258},[236,1993,1994],{"class":241},"application references to 'username') is fully deployed and verified.\n",[236,1996,1997],{"class":115,"line":264},[236,1998,249],{"emptyLinePlaceholder":248},[236,2000,2001],{"class":115,"line":270},[236,2002,2003],{"class":241},"Revision ID: e5f6a7b8c9d0\n",[236,2005,2006],{"class":115,"line":276},[236,2007,2008],{"class":241},"Revises: d4e5f6a7b8c9\n",[236,2010,2011],{"class":115,"line":294},[236,2012,2013],{"class":241},"Create Date: 2026-06-18 14:00:00.000000\n",[236,2015,2016],{"class":115,"line":299},[236,2017,273],{"class":241},[236,2019,2020,2022,2024,2026],{"class":115,"line":313},[236,2021,280],{"class":279},[236,2023,284],{"class":283},[236,2025,287],{"class":279},[236,2027,291],{"class":290},[236,2029,2030],{"class":115,"line":327},[236,2031,249],{"emptyLinePlaceholder":248},[236,2033,2034,2036,2038,2040],{"class":115,"line":332},[236,2035,280],{"class":279},[236,2037,304],{"class":290},[236,2039,307],{"class":279},[236,2041,310],{"class":290},[236,2043,2044,2046,2048,2050],{"class":115,"line":344},[236,2045,307],{"class":279},[236,2047,318],{"class":290},[236,2049,321],{"class":279},[236,2051,324],{"class":290},[236,2053,2054],{"class":115,"line":355},[236,2055,249],{"emptyLinePlaceholder":248},[236,2057,2058,2060,2062],{"class":115,"line":366},[236,2059,335],{"class":290},[236,2061,338],{"class":279},[236,2063,2064],{"class":241}," \"e5f6a7b8c9d0\"\n",[236,2066,2067,2069,2071],{"class":115,"line":376},[236,2068,347],{"class":290},[236,2070,338],{"class":279},[236,2072,1260],{"class":241},[236,2074,2075,2077,2079],{"class":115,"line":381},[236,2076,358],{"class":290},[236,2078,338],{"class":279},[236,2080,363],{"class":283},[236,2082,2083,2085,2087],{"class":115,"line":386},[236,2084,369],{"class":290},[236,2086,338],{"class":279},[236,2088,363],{"class":283},[236,2090,2091],{"class":115,"line":405},[236,2092,249],{"emptyLinePlaceholder":248},[236,2094,2095],{"class":115,"line":412},[236,2096,249],{"emptyLinePlaceholder":248},[236,2098,2099,2101,2103,2105,2107],{"class":115,"line":418},[236,2100,389],{"class":279},[236,2102,393],{"class":392},[236,2104,396],{"class":290},[236,2106,399],{"class":283},[236,2108,402],{"class":290},[236,2110,2111,2113,2115],{"class":115,"line":424},[236,2112,768],{"class":290},[236,2114,1326],{"class":241},[236,2116,1329],{"class":290},[236,2118,2119,2121,2124,2126,2129],{"class":115,"line":433},[236,2120,534],{"class":290},[236,2122,2123],{"class":241},"\"users\"",[236,2125,46],{"class":290},[236,2127,2128],{"class":241},"\"username\"",[236,2130,545],{"class":290},[236,2132,2133],{"class":115,"line":439},[236,2134,249],{"emptyLinePlaceholder":248},[236,2136,2137],{"class":115,"line":447},[236,2138,249],{"emptyLinePlaceholder":248},[236,2140,2141,2143,2145,2147,2149],{"class":115,"line":465},[236,2142,389],{"class":279},[236,2144,522],{"class":392},[236,2146,396],{"class":290},[236,2148,399],{"class":283},[236,2150,402],{"class":290},[236,2152,2153],{"class":115,"line":482},[236,2154,2155],{"class":408},"    # Restore the column (data loss — downgrade restores structure only).\n",[236,2157,2158],{"class":115,"line":495},[236,2159,421],{"class":290},[236,2161,2162,2165],{"class":115,"line":501},[236,2163,2164],{"class":241},"        \"users\"",[236,2166,430],{"class":290},[236,2168,2169,2171,2173,2175,2177,2179,2181,2183,2185,2187,2189],{"class":115,"line":507},[236,2170,1358],{"class":290},[236,2172,2128],{"class":241},[236,2174,1364],{"class":290},[236,2176,454],{"class":453},[236,2178,338],{"class":279},[236,2180,1743],{"class":283},[236,2182,1374],{"class":290},[236,2184,1377],{"class":453},[236,2186,338],{"class":279},[236,2188,473],{"class":283},[236,2190,462],{"class":290},[236,2192,2193],{"class":115,"line":512},[236,2194,504],{"class":290},[14,2196,2197,2198,2202,2203,1487,2206,2209],{},"Include the prerequisite deployment condition in the migration docstring so it is visible in version control history. Some teams enforce this with a migration comment convention checked by their CI pipeline. When reviewing ",[18,2199,2201],{"href":2200},"\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002F","autogenerated migration scripts",", search for ",[32,2204,2205],{},"drop_column",[32,2207,2208],{},"drop_table"," operations and verify they are guarded by the appropriate release sequencing.",[211,2211,2213],{"id":2212},"backfilling-large-tables","Backfilling Large Tables",[14,2215,2216,2217,2220,2221,2224],{},"Backfilling millions of rows in a single transaction holds locks for the entire duration of the ",[32,2218,2219],{},"UPDATE",". The zero-downtime approach is to batch the backfill using ",[32,2222,2223],{},"ctid"," ranges or primary key pagination, committing after each batch:",[227,2226,2228],{"className":229,"code":2227,"language":231,"meta":232,"style":232},"from __future__ import annotations\n\nimport asyncio\nfrom sqlalchemy import text, update\nfrom sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine\n\n\nasync def backfill_display_name_in_batches(engine: AsyncEngine) -> None:\n    \"\"\"Backfill display_name from username in batches of 5000 rows.\n\n    Run this as a one-off script during the migrate phase, not inside\n    an Alembic migration function, to allow safe interruption and resumption.\n    \"\"\"\n    batch_size = 5_000\n    last_id = 0\n\n    async with engine.connect() as conn:\n        while True:\n            result = await conn.execute(\n                text(\n                    \"UPDATE users SET display_name = username \"\n                    \"WHERE id > :last_id AND display_name IS NULL \"\n                    \"ORDER BY id LIMIT :batch_size \"\n                    \"RETURNING id\"\n                ),\n                {\"last_id\": last_id, \"batch_size\": batch_size},\n            )\n            rows = result.fetchall()\n            await conn.commit()\n\n            if not rows:\n                break\n\n            last_id = max(r[0] for r in rows)\n            print(f\"Backfilled up to id={last_id}\")\n            # Brief pause to reduce I\u002FO pressure on the primary\n            await asyncio.sleep(0.05)\n",[32,2229,2230,2240,2244,2251,2262,2273,2277,2281,2297,2302,2306,2311,2316,2321,2331,2341,2345,2361,2371,2384,2389,2394,2399,2404,2409,2414,2431,2436,2446,2454,2458,2469,2474,2478,2507,2534,2539],{"__ignoreMap":232},[236,2231,2232,2234,2236,2238],{"class":115,"line":238},[236,2233,280],{"class":279},[236,2235,284],{"class":283},[236,2237,287],{"class":279},[236,2239,291],{"class":290},[236,2241,2242],{"class":115,"line":245},[236,2243,249],{"emptyLinePlaceholder":248},[236,2245,2246,2248],{"class":115,"line":252},[236,2247,307],{"class":279},[236,2249,2250],{"class":290}," asyncio\n",[236,2252,2253,2255,2257,2259],{"class":115,"line":258},[236,2254,280],{"class":279},[236,2256,318],{"class":290},[236,2258,307],{"class":279},[236,2260,2261],{"class":290}," text, update\n",[236,2263,2264,2266,2268,2270],{"class":115,"line":264},[236,2265,280],{"class":279},[236,2267,1603],{"class":290},[236,2269,307],{"class":279},[236,2271,2272],{"class":290}," AsyncEngine, create_async_engine\n",[236,2274,2275],{"class":115,"line":270},[236,2276,249],{"emptyLinePlaceholder":248},[236,2278,2279],{"class":115,"line":276},[236,2280,249],{"emptyLinePlaceholder":248},[236,2282,2283,2285,2287,2290,2293,2295],{"class":115,"line":294},[236,2284,1800],{"class":279},[236,2286,1803],{"class":279},[236,2288,2289],{"class":392}," backfill_display_name_in_batches",[236,2291,2292],{"class":290},"(engine: AsyncEngine) -> ",[236,2294,399],{"class":283},[236,2296,402],{"class":290},[236,2298,2299],{"class":115,"line":299},[236,2300,2301],{"class":241},"    \"\"\"Backfill display_name from username in batches of 5000 rows.\n",[236,2303,2304],{"class":115,"line":313},[236,2305,249],{"emptyLinePlaceholder":248},[236,2307,2308],{"class":115,"line":327},[236,2309,2310],{"class":241},"    Run this as a one-off script during the migrate phase, not inside\n",[236,2312,2313],{"class":115,"line":332},[236,2314,2315],{"class":241},"    an Alembic migration function, to allow safe interruption and resumption.\n",[236,2317,2318],{"class":115,"line":344},[236,2319,2320],{"class":241},"    \"\"\"\n",[236,2322,2323,2326,2328],{"class":115,"line":355},[236,2324,2325],{"class":290},"    batch_size ",[236,2327,338],{"class":279},[236,2329,2330],{"class":283}," 5_000\n",[236,2332,2333,2336,2338],{"class":115,"line":366},[236,2334,2335],{"class":290},"    last_id ",[236,2337,338],{"class":279},[236,2339,2340],{"class":283}," 0\n",[236,2342,2343],{"class":115,"line":376},[236,2344,249],{"emptyLinePlaceholder":248},[236,2346,2347,2350,2353,2356,2358],{"class":115,"line":381},[236,2348,2349],{"class":279},"    async",[236,2351,2352],{"class":279}," with",[236,2354,2355],{"class":290}," engine.connect() ",[236,2357,321],{"class":279},[236,2359,2360],{"class":290}," conn:\n",[236,2362,2363,2366,2369],{"class":115,"line":386},[236,2364,2365],{"class":279},"        while",[236,2367,2368],{"class":283}," True",[236,2370,402],{"class":290},[236,2372,2373,2376,2378,2381],{"class":115,"line":405},[236,2374,2375],{"class":290},"            result ",[236,2377,338],{"class":279},[236,2379,2380],{"class":279}," await",[236,2382,2383],{"class":290}," conn.execute(\n",[236,2385,2386],{"class":115,"line":412},[236,2387,2388],{"class":290},"                text(\n",[236,2390,2391],{"class":115,"line":418},[236,2392,2393],{"class":241},"                    \"UPDATE users SET display_name = username \"\n",[236,2395,2396],{"class":115,"line":424},[236,2397,2398],{"class":241},"                    \"WHERE id > :last_id AND display_name IS NULL \"\n",[236,2400,2401],{"class":115,"line":433},[236,2402,2403],{"class":241},"                    \"ORDER BY id LIMIT :batch_size \"\n",[236,2405,2406],{"class":115,"line":439},[236,2407,2408],{"class":241},"                    \"RETURNING id\"\n",[236,2410,2411],{"class":115,"line":447},[236,2412,2413],{"class":290},"                ),\n",[236,2415,2416,2419,2422,2425,2428],{"class":115,"line":465},[236,2417,2418],{"class":290},"                {",[236,2420,2421],{"class":241},"\"last_id\"",[236,2423,2424],{"class":290},": last_id, ",[236,2426,2427],{"class":241},"\"batch_size\"",[236,2429,2430],{"class":290},": batch_size},\n",[236,2432,2433],{"class":115,"line":482},[236,2434,2435],{"class":290},"            )\n",[236,2437,2438,2441,2443],{"class":115,"line":495},[236,2439,2440],{"class":290},"            rows ",[236,2442,338],{"class":279},[236,2444,2445],{"class":290}," result.fetchall()\n",[236,2447,2448,2451],{"class":115,"line":501},[236,2449,2450],{"class":279},"            await",[236,2452,2453],{"class":290}," conn.commit()\n",[236,2455,2456],{"class":115,"line":507},[236,2457,249],{"emptyLinePlaceholder":248},[236,2459,2460,2463,2466],{"class":115,"line":512},[236,2461,2462],{"class":279},"            if",[236,2464,2465],{"class":279}," not",[236,2467,2468],{"class":290}," rows:\n",[236,2470,2471],{"class":115,"line":517},[236,2472,2473],{"class":279},"                break\n",[236,2475,2476],{"class":115,"line":531},[236,2477,249],{"emptyLinePlaceholder":248},[236,2479,2480,2483,2485,2488,2491,2493,2495,2498,2501,2504],{"class":115,"line":847},[236,2481,2482],{"class":290},"            last_id ",[236,2484,338],{"class":279},[236,2486,2487],{"class":283}," max",[236,2489,2490],{"class":290},"(r[",[236,2492,109],{"class":283},[236,2494,1700],{"class":290},[236,2496,2497],{"class":279},"for",[236,2499,2500],{"class":290}," r ",[236,2502,2503],{"class":279},"in",[236,2505,2506],{"class":290}," rows)\n",[236,2508,2509,2512,2514,2517,2520,2523,2526,2529,2532],{"class":115,"line":860},[236,2510,2511],{"class":283},"            print",[236,2513,1639],{"class":290},[236,2515,2516],{"class":279},"f",[236,2518,2519],{"class":241},"\"Backfilled up to id=",[236,2521,2522],{"class":283},"{",[236,2524,2525],{"class":290},"last_id",[236,2527,2528],{"class":283},"}",[236,2530,2531],{"class":241},"\"",[236,2533,545],{"class":290},[236,2535,2536],{"class":115,"line":869},[236,2537,2538],{"class":408},"            # Brief pause to reduce I\u002FO pressure on the primary\n",[236,2540,2541,2543,2546,2549],{"class":115,"line":882},[236,2542,2450],{"class":279},[236,2544,2545],{"class":290}," asyncio.sleep(",[236,2547,2548],{"class":283},"0.05",[236,2550,545],{"class":290},[14,2552,2553,2554,1510,2557,2560],{},"Run this backfill script outside of Alembic's migration runner — as a standalone administrative script — so it can be interrupted, monitored, and resumed safely. Place it in a ",[32,2555,2556],{},"scripts\u002F",[32,2558,2559],{},"migrations\u002Fbackfills\u002F"," directory with a comment linking to the corresponding Alembic revision.",[24,2562,2564],{"id":2563},"hybrid-strategies-14-20-migration-path","Hybrid Strategies & 1.4 → 2.0 Migration Path",[211,2566,2568],{"id":2567},"opbatch_alter_table-for-sqlite","op.batch_alter_table for SQLite",[14,2570,2571,2572,2575,2576,2578,2579,2582],{},"SQLite does not support ",[32,2573,2574],{},"ALTER TABLE ADD COLUMN"," with constraints, foreign keys, or ",[32,2577,34],{}," in the same way PostgreSQL does. Alembic's ",[32,2580,2581],{},"op.batch_alter_table"," handles SQLite by performing a table recreation: it creates a new table with the target schema, copies data, drops the old table, and renames the new one. This is safe for development and test environments but is not suitable for production zero-downtime migrations on PostgreSQL.",[227,2584,2586],{"className":229,"code":2585,"language":231,"meta":232,"style":232},"\"\"\"SQLite-compatible migration using batch_alter_table.\n\nOn PostgreSQL, prefer native op.add_column \u002F op.drop_column.\nOn SQLite (dev\u002Ftest), batch_alter_table handles schema reconstruction.\n\nRevision ID: f6a7b8c9d0e1\nRevises: e5f6a7b8c9d0\nCreate Date: 2026-06-18 15:00:00.000000\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy.engine import Inspector\n\nrevision = \"f6a7b8c9d0e1\"\ndown_revision = \"e5f6a7b8c9d0\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    bind = op.get_bind()\n    if bind.dialect.name == \"sqlite\":\n        # SQLite requires table recreation for constraint changes.\n        with op.batch_alter_table(\"products\") as batch_op:\n            batch_op.add_column(\n                sa.Column(\"sku\", sa.String(length=64), nullable=True)\n            )\n    else:\n        # PostgreSQL: native ALTER TABLE, no rewrite needed.\n        op.execute(sa.text(\"SET lock_timeout = '2s'\"))\n        op.add_column(\n            \"products\",\n            sa.Column(\"sku\", sa.String(length=64), nullable=True),\n        )\n\n\ndef downgrade() -> None:\n    bind = op.get_bind()\n    if bind.dialect.name == \"sqlite\":\n        with op.batch_alter_table(\"products\") as batch_op:\n            batch_op.drop_column(\"sku\")\n    else:\n        op.execute(sa.text(\"SET lock_timeout = '2s'\"))\n        op.drop_column(\"products\", \"sku\")\n",[32,2587,2588,2593,2597,2602,2607,2611,2616,2621,2626,2630,2640,2644,2654,2664,2676,2680,2689,2697,2705,2713,2717,2721,2733,2742,2757,2762,2781,2786,2813,2817,2824,2829,2838,2843,2850,2875,2879,2883,2887,2899,2907,2920,2935,2945,2952,2961],{"__ignoreMap":232},[236,2589,2590],{"class":115,"line":238},[236,2591,2592],{"class":241},"\"\"\"SQLite-compatible migration using batch_alter_table.\n",[236,2594,2595],{"class":115,"line":245},[236,2596,249],{"emptyLinePlaceholder":248},[236,2598,2599],{"class":115,"line":252},[236,2600,2601],{"class":241},"On PostgreSQL, prefer native op.add_column \u002F op.drop_column.\n",[236,2603,2604],{"class":115,"line":258},[236,2605,2606],{"class":241},"On SQLite (dev\u002Ftest), batch_alter_table handles schema reconstruction.\n",[236,2608,2609],{"class":115,"line":264},[236,2610,249],{"emptyLinePlaceholder":248},[236,2612,2613],{"class":115,"line":270},[236,2614,2615],{"class":241},"Revision ID: f6a7b8c9d0e1\n",[236,2617,2618],{"class":115,"line":276},[236,2619,2620],{"class":241},"Revises: e5f6a7b8c9d0\n",[236,2622,2623],{"class":115,"line":294},[236,2624,2625],{"class":241},"Create Date: 2026-06-18 15:00:00.000000\n",[236,2627,2628],{"class":115,"line":299},[236,2629,273],{"class":241},[236,2631,2632,2634,2636,2638],{"class":115,"line":313},[236,2633,280],{"class":279},[236,2635,284],{"class":283},[236,2637,287],{"class":279},[236,2639,291],{"class":290},[236,2641,2642],{"class":115,"line":327},[236,2643,249],{"emptyLinePlaceholder":248},[236,2645,2646,2648,2650,2652],{"class":115,"line":332},[236,2647,280],{"class":279},[236,2649,304],{"class":290},[236,2651,307],{"class":279},[236,2653,310],{"class":290},[236,2655,2656,2658,2660,2662],{"class":115,"line":344},[236,2657,307],{"class":279},[236,2659,318],{"class":290},[236,2661,321],{"class":279},[236,2663,324],{"class":290},[236,2665,2666,2668,2671,2673],{"class":115,"line":355},[236,2667,280],{"class":279},[236,2669,2670],{"class":290}," sqlalchemy.engine ",[236,2672,307],{"class":279},[236,2674,2675],{"class":290}," Inspector\n",[236,2677,2678],{"class":115,"line":366},[236,2679,249],{"emptyLinePlaceholder":248},[236,2681,2682,2684,2686],{"class":115,"line":376},[236,2683,335],{"class":290},[236,2685,338],{"class":279},[236,2687,2688],{"class":241}," \"f6a7b8c9d0e1\"\n",[236,2690,2691,2693,2695],{"class":115,"line":381},[236,2692,347],{"class":290},[236,2694,338],{"class":279},[236,2696,2064],{"class":241},[236,2698,2699,2701,2703],{"class":115,"line":386},[236,2700,358],{"class":290},[236,2702,338],{"class":279},[236,2704,363],{"class":283},[236,2706,2707,2709,2711],{"class":115,"line":405},[236,2708,369],{"class":290},[236,2710,338],{"class":279},[236,2712,363],{"class":283},[236,2714,2715],{"class":115,"line":412},[236,2716,249],{"emptyLinePlaceholder":248},[236,2718,2719],{"class":115,"line":418},[236,2720,249],{"emptyLinePlaceholder":248},[236,2722,2723,2725,2727,2729,2731],{"class":115,"line":424},[236,2724,389],{"class":279},[236,2726,393],{"class":392},[236,2728,396],{"class":290},[236,2730,399],{"class":283},[236,2732,402],{"class":290},[236,2734,2735,2738,2740],{"class":115,"line":433},[236,2736,2737],{"class":290},"    bind ",[236,2739,338],{"class":279},[236,2741,796],{"class":290},[236,2743,2744,2747,2750,2752,2755],{"class":115,"line":439},[236,2745,2746],{"class":279},"    if",[236,2748,2749],{"class":290}," bind.dialect.name ",[236,2751,1857],{"class":279},[236,2753,2754],{"class":241}," \"sqlite\"",[236,2756,402],{"class":290},[236,2758,2759],{"class":115,"line":447},[236,2760,2761],{"class":408},"        # SQLite requires table recreation for constraint changes.\n",[236,2763,2764,2767,2770,2773,2776,2778],{"class":115,"line":465},[236,2765,2766],{"class":279},"        with",[236,2768,2769],{"class":290}," op.batch_alter_table(",[236,2771,2772],{"class":241},"\"products\"",[236,2774,2775],{"class":290},") ",[236,2777,321],{"class":279},[236,2779,2780],{"class":290}," batch_op:\n",[236,2782,2783],{"class":115,"line":482},[236,2784,2785],{"class":290},"            batch_op.add_column(\n",[236,2787,2788,2791,2794,2796,2798,2800,2803,2805,2807,2809,2811],{"class":115,"line":495},[236,2789,2790],{"class":290},"                sa.Column(",[236,2792,2793],{"class":241},"\"sku\"",[236,2795,1364],{"class":290},[236,2797,454],{"class":453},[236,2799,338],{"class":279},[236,2801,2802],{"class":283},"64",[236,2804,1374],{"class":290},[236,2806,1377],{"class":453},[236,2808,338],{"class":279},[236,2810,473],{"class":283},[236,2812,545],{"class":290},[236,2814,2815],{"class":115,"line":501},[236,2816,2435],{"class":290},[236,2818,2819,2822],{"class":115,"line":507},[236,2820,2821],{"class":279},"    else",[236,2823,402],{"class":290},[236,2825,2826],{"class":115,"line":512},[236,2827,2828],{"class":408},"        # PostgreSQL: native ALTER TABLE, no rewrite needed.\n",[236,2830,2831,2834,2836],{"class":115,"line":517},[236,2832,2833],{"class":290},"        op.execute(sa.text(",[236,2835,1326],{"class":241},[236,2837,1329],{"class":290},[236,2839,2840],{"class":115,"line":531},[236,2841,2842],{"class":290},"        op.add_column(\n",[236,2844,2845,2848],{"class":115,"line":847},[236,2846,2847],{"class":241},"            \"products\"",[236,2849,430],{"class":290},[236,2851,2852,2855,2857,2859,2861,2863,2865,2867,2869,2871,2873],{"class":115,"line":860},[236,2853,2854],{"class":290},"            sa.Column(",[236,2856,2793],{"class":241},[236,2858,1364],{"class":290},[236,2860,454],{"class":453},[236,2862,338],{"class":279},[236,2864,2802],{"class":283},[236,2866,1374],{"class":290},[236,2868,1377],{"class":453},[236,2870,338],{"class":279},[236,2872,473],{"class":283},[236,2874,462],{"class":290},[236,2876,2877],{"class":115,"line":869},[236,2878,832],{"class":290},[236,2880,2881],{"class":115,"line":882},[236,2882,249],{"emptyLinePlaceholder":248},[236,2884,2885],{"class":115,"line":893},[236,2886,249],{"emptyLinePlaceholder":248},[236,2888,2889,2891,2893,2895,2897],{"class":115,"line":1466},[236,2890,389],{"class":279},[236,2892,522],{"class":392},[236,2894,396],{"class":290},[236,2896,399],{"class":283},[236,2898,402],{"class":290},[236,2900,2901,2903,2905],{"class":115,"line":1475},[236,2902,2737],{"class":290},[236,2904,338],{"class":279},[236,2906,796],{"class":290},[236,2908,2910,2912,2914,2916,2918],{"class":115,"line":2909},41,[236,2911,2746],{"class":279},[236,2913,2749],{"class":290},[236,2915,1857],{"class":279},[236,2917,2754],{"class":241},[236,2919,402],{"class":290},[236,2921,2923,2925,2927,2929,2931,2933],{"class":115,"line":2922},42,[236,2924,2766],{"class":279},[236,2926,2769],{"class":290},[236,2928,2772],{"class":241},[236,2930,2775],{"class":290},[236,2932,321],{"class":279},[236,2934,2780],{"class":290},[236,2936,2938,2941,2943],{"class":115,"line":2937},43,[236,2939,2940],{"class":290},"            batch_op.drop_column(",[236,2942,2793],{"class":241},[236,2944,545],{"class":290},[236,2946,2948,2950],{"class":115,"line":2947},44,[236,2949,2821],{"class":279},[236,2951,402],{"class":290},[236,2953,2955,2957,2959],{"class":115,"line":2954},45,[236,2956,2833],{"class":290},[236,2958,1326],{"class":241},[236,2960,1329],{"class":290},[236,2962,2964,2967,2969,2971,2973],{"class":115,"line":2963},46,[236,2965,2966],{"class":290},"        op.drop_column(",[236,2968,2772],{"class":241},[236,2970,46],{"class":290},[236,2972,2793],{"class":241},[236,2974,545],{"class":290},[211,2976,2978],{"id":2977},"migrating-14-migration-scripts-to-20-patterns","Migrating 1.4 Migration Scripts to 2.0 Patterns",[14,2980,2981,2982,46,2985,2988,2989,2992,2993,2995,2996,2999,3000,3003,3004,3007,3008,3011],{},"Alembic migration files authored against SQLAlchemy 1.4 are almost always forward-compatible: ",[32,2983,2984],{},"op.add_column",[32,2986,2987],{},"op.drop_column",", and ",[32,2990,2991],{},"op.create_index"," function identically in 2.0. The main change is in ",[32,2994,1157],{}," — specifically, replacing synchronous ",[32,2997,2998],{},"engine_from_config"," with ",[32,3001,3002],{},"create_async_engine"," and wrapping ",[32,3005,3006],{},"context.run_migrations()"," in ",[32,3009,3010],{},"asyncio.run(run_async_migrations())",". The migration script files themselves rarely need edits.",[14,3013,3014,3015,3018,3019,3022,3023,3026,3027,3029],{},"However, if your 1.4 migrations use ",[32,3016,3017],{},"op.get_bind().execute(string_sql)"," with raw string queries (rather than ",[32,3020,3021],{},"sa.text(...)","), these will raise ",[32,3024,3025],{},"ObjectNotExecutableError"," in SQLAlchemy 2.0 because plain strings are no longer accepted as executable. Replace every bare string with ",[32,3028,3021],{},":",[227,3031,3033],{"className":229,"code":3032,"language":231,"meta":232,"style":232},"# 1.4 pattern — raises ObjectNotExecutableError in 2.0\nop.get_bind().execute(\"UPDATE orders SET status = 'legacy' WHERE status IS NULL\")\n\n# 2.0 compatible\nop.execute(sa.text(\"UPDATE orders SET status = 'legacy' WHERE status IS NULL\"))\n",[32,3034,3035,3040,3050,3054,3059],{"__ignoreMap":232},[236,3036,3037],{"class":115,"line":238},[236,3038,3039],{"class":408},"# 1.4 pattern — raises ObjectNotExecutableError in 2.0\n",[236,3041,3042,3045,3048],{"class":115,"line":245},[236,3043,3044],{"class":290},"op.get_bind().execute(",[236,3046,3047],{"class":241},"\"UPDATE orders SET status = 'legacy' WHERE status IS NULL\"",[236,3049,545],{"class":290},[236,3051,3052],{"class":115,"line":252},[236,3053,249],{"emptyLinePlaceholder":248},[236,3055,3056],{"class":115,"line":258},[236,3057,3058],{"class":408},"# 2.0 compatible\n",[236,3060,3061,3064,3066],{"class":115,"line":264},[236,3062,3063],{"class":290},"op.execute(sa.text(",[236,3065,3047],{"class":241},[236,3067,1329],{"class":290},[14,3069,3070,3071,3073,3074,22],{},"The full ",[32,3072,1157],{}," configuration for async engines is covered in ",[18,3075,1153],{"href":1152},[24,3077,3079],{"id":3078},"production-pitfalls-anti-patterns","Production Pitfalls & Anti-Patterns",[65,3081,3082,3092,3118,3149,3162,3182],{},[68,3083,3084,3087,3088,3091],{},[71,3085,3086],{},"Dropping a column in the same release that removes it from application code."," If any instance of the previous release is still running when the migration executes, reads against the dropped column will raise ",[32,3089,3090],{},"UndefinedColumnError",". Always ship the drop migration one release after the code removal.",[68,3093,3094,3101,3102,1145,3104,3106,3107,3109,3110,3112,3113,1510,3115,22],{},[71,3095,3096,3097,3100],{},"Using ",[32,3098,3099],{},"op.execute(\"CREATE INDEX ...\")"," inside a transactional migration."," Alembic wraps migrations in ",[32,3103,1144],{},[32,3105,1148],{}," by default. ",[32,3108,593],{}," inside a transaction raises ",[32,3111,909],{},". Use ",[32,3114,1140],{},[32,3116,3117],{},"op.get_bind().execution_options(isolation_level=\"AUTOCOMMIT\")",[68,3119,3120,3126,3127,3130,3131,3133,3134,3137,3138,3141,3142,3144,3145,3148],{},[71,3121,3122,3123,3125],{},"Applying a ",[32,3124,34],{}," constraint without a prior backfill."," ",[32,3128,3129],{},"ALTER TABLE ALTER COLUMN SET NOT NULL"," acquires ",[32,3132,38],{}," and scans every row to verify nullability. On a table with unfilled rows, this fails immediately with a constraint violation. Use ",[32,3135,3136],{},"ADD CONSTRAINT ... NOT VALID"," followed by ",[32,3139,3140],{},"VALIDATE CONSTRAINT"," in a separate step — ",[32,3143,3140],{}," uses a weaker ",[32,3146,3147],{},"ShareUpdateExclusiveLock"," that does not block reads.",[68,3150,3151,3126,3154,3156,3157,3159,3160,22],{},[71,3152,3153],{},"Mixing concurrent index creation with other DDL in the same migration.",[32,3155,593],{}," cannot share a transaction with ",[32,3158,45],{}," or other DDL. Isolate concurrent index migrations in their own revision file with ",[32,3161,1140],{},[68,3163,3164,3171,3172,3175,3176,3178,3179,3181],{},[71,3165,3166,3167,3170],{},"Relying on ",[32,3168,3169],{},"op.execute(sa.text(\"COMMIT\"))"," as the sole mechanism to escape the transaction."," This commits the Alembic transaction prematurely, leaving the migration state partially updated in ",[32,3173,3174],{},"alembic_version",". If the migration fails after the manual ",[32,3177,1148],{},", the revision record may not be written. Use ",[32,3180,1140],{}," instead to keep Alembic's bookkeeping intact.",[68,3183,3184,3193,3194,3196,3197,3199,3200,3202,3203,3206],{},[71,3185,1137,3186,3188,3189,3192],{},[32,3187,1179],{}," globally in ",[32,3190,3191],{},"postgresql.conf"," instead of per-migration."," A global ",[32,3195,1179],{}," that is too aggressive will abort legitimate long-running analytical queries. Set ",[32,3198,1179],{}," at the session level inside ",[32,3201,1494],{}," and reset it with ",[32,3204,3205],{},"SET statement_timeout = DEFAULT"," before the function returns.",[24,3208,3210],{"id":3209},"frequently-asked-questions","Frequently Asked Questions",[14,3212,3213],{},[71,3214,3215],{},"Can I add multiple columns in a single Alembic migration without locking?",[14,3217,3218,3219,3222,3223,3225,3226,3228,3229,3231,3232,3234,3235,3237],{},"Yes, for PostgreSQL 11+. Each ",[32,3220,3221],{},"ADD COLUMN"," with a ",[32,3224,551],{}," is a metadata-only operation that avoids table rewrites and requires only a brief ",[32,3227,38],{}," to update the catalog. Combining multiple ",[32,3230,3221],{}," statements in a single ",[32,3233,45],{}," reduces the number of lock acquisitions to one. Use ",[32,3236,2984],{}," calls within the same migration, and Alembic will batch them if the dialect supports it. Nullable columns without defaults are always instant.",[14,3239,3240],{},[71,3241,3242,3243,3245],{},"What happens if ",[32,3244,593],{}," fails partway through?",[14,3247,3248,3249,3007,3252,3255,3256,3259,3260,3263,3264,3267],{},"PostgreSQL marks the index as ",[32,3250,3251],{},"INVALID",[32,3253,3254],{},"pg_index",". The index consumes space but is not used by the query planner. You must ",[32,3257,3258],{},"DROP INDEX"," the invalid index and retry. Use ",[32,3261,3262],{},"IF NOT EXISTS"," in your migration and check ",[32,3265,3266],{},"pg_indexes"," for invalid entries before retrying. Alembic does not automatically clean up invalid indexes.",[14,3269,3270],{},[71,3271,3272,3273,3275],{},"How do I handle a migration that times out because ",[32,3274,1175],{}," fires?",[14,3277,3278,3279,3281,3282,3284,3285,1487,3288,22],{},"The migration fails with ",[32,3280,1498],{},". Alembic rolls back the transaction (or the operation fails without rollback if using ",[32,3283,1140],{},"). Your deployment pipeline should treat this as a retriable failure. Schedule a retry during a lower-traffic window, or investigate the long-running query holding the conflicting lock using ",[32,3286,3287],{},"pg_stat_activity",[32,3289,3290],{},"pg_locks",[14,3292,3293],{},[71,3294,3295,3296,3299],{},"Is it safe to run Alembic ",[32,3297,3298],{},"upgrade head"," in a Kubernetes init container?",[14,3301,3302],{},"Yes, with caveats. Multiple replicas starting simultaneously may attempt to run migrations concurrently. Alembic does not provide distributed locking out of the box. Use an advisory lock at the start of your migration runner:",[227,3304,3306],{"className":229,"code":3305,"language":231,"meta":232,"style":232},"import asyncio\nfrom sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import create_async_engine\nfrom alembic import command\nfrom alembic.config import Config\n\n\nasync def run_migrations_with_advisory_lock() -> None:\n    engine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@host\u002Fdb\")\n    async with engine.connect() as conn:\n        # pg_try_advisory_lock returns true only for the first caller.\n        # Other callers get false and should skip migration.\n        result = await conn.execute(text(\"SELECT pg_try_advisory_lock(12345)\"))\n        acquired = result.scalar()\n        if not acquired:\n            print(\"Another instance is running migrations — skipping.\")\n            return\n\n    cfg = Config(\"alembic.ini\")\n    command.upgrade(cfg, \"head\")\n",[32,3307,3308,3314,3325,3336,3347,3359,3363,3367,3382,3397,3409,3414,3419,3436,3446,3456,3467,3472,3476,3491],{"__ignoreMap":232},[236,3309,3310,3312],{"class":115,"line":238},[236,3311,307],{"class":279},[236,3313,2250],{"class":290},[236,3315,3316,3318,3320,3322],{"class":115,"line":245},[236,3317,280],{"class":279},[236,3319,318],{"class":290},[236,3321,307],{"class":279},[236,3323,3324],{"class":290}," text\n",[236,3326,3327,3329,3331,3333],{"class":115,"line":252},[236,3328,280],{"class":279},[236,3330,1603],{"class":290},[236,3332,307],{"class":279},[236,3334,3335],{"class":290}," create_async_engine\n",[236,3337,3338,3340,3342,3344],{"class":115,"line":258},[236,3339,280],{"class":279},[236,3341,304],{"class":290},[236,3343,307],{"class":279},[236,3345,3346],{"class":290}," command\n",[236,3348,3349,3351,3354,3356],{"class":115,"line":264},[236,3350,280],{"class":279},[236,3352,3353],{"class":290}," alembic.config ",[236,3355,307],{"class":279},[236,3357,3358],{"class":290}," Config\n",[236,3360,3361],{"class":115,"line":270},[236,3362,249],{"emptyLinePlaceholder":248},[236,3364,3365],{"class":115,"line":276},[236,3366,249],{"emptyLinePlaceholder":248},[236,3368,3369,3371,3373,3376,3378,3380],{"class":115,"line":294},[236,3370,1800],{"class":279},[236,3372,1803],{"class":279},[236,3374,3375],{"class":392}," run_migrations_with_advisory_lock",[236,3377,396],{"class":290},[236,3379,399],{"class":283},[236,3381,402],{"class":290},[236,3383,3384,3387,3389,3392,3395],{"class":115,"line":299},[236,3385,3386],{"class":290},"    engine ",[236,3388,338],{"class":279},[236,3390,3391],{"class":290}," create_async_engine(",[236,3393,3394],{"class":241},"\"postgresql+asyncpg:\u002F\u002Fuser:pass@host\u002Fdb\"",[236,3396,545],{"class":290},[236,3398,3399,3401,3403,3405,3407],{"class":115,"line":313},[236,3400,2349],{"class":279},[236,3402,2352],{"class":279},[236,3404,2355],{"class":290},[236,3406,321],{"class":279},[236,3408,2360],{"class":290},[236,3410,3411],{"class":115,"line":327},[236,3412,3413],{"class":408},"        # pg_try_advisory_lock returns true only for the first caller.\n",[236,3415,3416],{"class":115,"line":332},[236,3417,3418],{"class":408},"        # Other callers get false and should skip migration.\n",[236,3420,3421,3424,3426,3428,3431,3434],{"class":115,"line":344},[236,3422,3423],{"class":290},"        result ",[236,3425,338],{"class":279},[236,3427,2380],{"class":279},[236,3429,3430],{"class":290}," conn.execute(text(",[236,3432,3433],{"class":241},"\"SELECT pg_try_advisory_lock(12345)\"",[236,3435,1329],{"class":290},[236,3437,3438,3441,3443],{"class":115,"line":355},[236,3439,3440],{"class":290},"        acquired ",[236,3442,338],{"class":279},[236,3444,3445],{"class":290}," result.scalar()\n",[236,3447,3448,3451,3453],{"class":115,"line":366},[236,3449,3450],{"class":279},"        if",[236,3452,2465],{"class":279},[236,3454,3455],{"class":290}," acquired:\n",[236,3457,3458,3460,3462,3465],{"class":115,"line":376},[236,3459,2511],{"class":283},[236,3461,1639],{"class":290},[236,3463,3464],{"class":241},"\"Another instance is running migrations — skipping.\"",[236,3466,545],{"class":290},[236,3468,3469],{"class":115,"line":381},[236,3470,3471],{"class":279},"            return\n",[236,3473,3474],{"class":115,"line":386},[236,3475,249],{"emptyLinePlaceholder":248},[236,3477,3478,3481,3483,3486,3489],{"class":115,"line":405},[236,3479,3480],{"class":290},"    cfg ",[236,3482,338],{"class":279},[236,3484,3485],{"class":290}," Config(",[236,3487,3488],{"class":241},"\"alembic.ini\"",[236,3490,545],{"class":290},[236,3492,3493,3496,3499],{"class":115,"line":412},[236,3494,3495],{"class":290},"    command.upgrade(cfg, ",[236,3497,3498],{"class":241},"\"head\"",[236,3500,545],{"class":290},[14,3502,3503],{},[71,3504,3505,3506,3508],{},"Does ",[32,3507,2581],{}," work safely on PostgreSQL?",[14,3510,3511,3513,3514,3516,3517,3520,3521,22],{},[32,3512,2581],{}," performs a table recreation on SQLite, but on PostgreSQL it delegates to native ",[32,3515,45],{}," statements. It is safe on PostgreSQL and produces identical results to the native operations. The main use case for ",[32,3518,3519],{},"batch_alter_table"," on PostgreSQL is writing dialect-agnostic migrations that work in both environments without branching on ",[32,3522,3523],{},"bind.dialect.name",[14,3525,3526],{},[71,3527,3528],{},"How do I verify that a migration is backwards-compatible before deploying?",[14,3530,3531,3532,3535,3536,1510,3538,3540,3541,3543,3544,3546,3547,3549,3550,3552],{},"Review the generated migration with the ",[18,3533,3534],{"href":2200},"autogenerate workflow"," and check for: any ",[32,3537,2205],{},[32,3539,2208],{}," operations (require prior code removal), any ",[32,3542,34],{}," constraint additions without a preceding backfill, any ",[32,3545,1509],{}," without ",[32,3548,599],{},", and any missing ",[32,3551,1175],{}," guards on tables above a few million rows.",[24,3554,3556],{"id":3555},"related","Related",[65,3558,3559,3575,3585,3598,3605],{},[68,3560,3561,3564,3565,3567,3568,3571,3572,3574],{},[18,3562,3563],{"href":1152},"Configuring Alembic with Async SQLAlchemy Engines"," — setting up ",[32,3566,1157],{}," to drive Alembic migrations through an ",[32,3569,3570],{},"asyncpg","-backed async engine, including the ",[32,3573,919],{}," pattern and connection lifecycle management.",[68,3576,3577,3580,3581,3584],{},[18,3578,3579],{"href":2200},"Autogenerating and Reviewing Migration Scripts"," — how to use ",[32,3582,3583],{},"alembic revision --autogenerate",", what it cannot detect automatically, and the review checklist for catching destructive or lock-heavy operations before they reach production.",[68,3586,3587,3590,3591,3593,3594,3597],{},[18,3588,3589],{"href":588},"Adding a NOT NULL Column Without Locking in Postgres"," — the three-step sequence for safely constraining a column to ",[32,3592,34],{}," on a large live table using ",[32,3595,3596],{},"NOT VALID"," constraints and deferred validation.",[68,3599,3600,3604],{},[18,3601,3603],{"href":3602},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002Fsetting-up-alembic-env-py-for-asyncpg\u002F","Setting Up asyncpg Connection Pool Size for High Concurrency"," — tuning the connection pool available to your migration runner and application layer for concurrent workloads.",[68,3606,3607,3610],{},[18,3608,3609],{"href":20},"Alembic Async Migrations and Schema Evolution"," — the parent reference covering the full scope of Alembic + async SQLAlchemy: engine setup, script generation, migration execution, and schema evolution patterns.",[3612,3613,3614],"style",{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":232,"searchDepth":245,"depth":245,"links":3616},[3617,3618,3622,3626,3632,3636,3637,3638],{"id":26,"depth":245,"text":27},{"id":208,"depth":245,"text":209,"children":3619},[3620,3621],{"id":213,"depth":252,"text":214},{"id":592,"depth":252,"text":593},{"id":1161,"depth":245,"text":1162,"children":3623},[3624,3625],{"id":1165,"depth":252,"text":1166},{"id":1502,"depth":252,"text":1503},{"id":1537,"depth":245,"text":1538,"children":3627},[3628,3629,3630,3631],{"id":1541,"depth":252,"text":1542},{"id":1920,"depth":252,"text":1921},{"id":1961,"depth":252,"text":1962},{"id":2212,"depth":252,"text":2213},{"id":2563,"depth":245,"text":2564,"children":3633},[3634,3635],{"id":2567,"depth":252,"text":2568},{"id":2977,"depth":252,"text":2978},{"id":3078,"depth":245,"text":3079},{"id":3209,"depth":245,"text":3210},{"id":3555,"depth":245,"text":3556},"Running schema changes against a live PostgreSQL database without downtime requires deliberate sequencing, careful lock management, and a disciplined split between schema evolution and application deployment. The core principle is that your database schema and your application code must remain mutually compatible across at least two consecutive releases — the current version and the one being rolled out. This guide covers the expand-contract pattern, safe column additions, concurrent index builds, lock timeout guards, and how to orchestrate destructive changes across separate deployments using Alembic with async SQLAlchemy engines.","md",{"date":3642},"2026-06-18","\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies",{"title":5,"description":3639},"alembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Findex","DeI8ovfTuMQCpQWMSnI6x0LxLgU9tPz4J8bndHQ7XpY",1781810028982]