[{"data":1,"prerenderedAt":5403},["ShallowReactive",2],{"page-\u002Falembic-async-migrations-and-schema-evolution\u002F":3},{"id":4,"title":5,"body":6,"description":5395,"extension":5396,"meta":5397,"navigation":313,"path":5399,"seo":5400,"stem":5401,"__hash__":5402},"content\u002Falembic-async-migrations-and-schema-evolution\u002Findex.md","Alembic Async Migrations and Schema Evolution for SQLAlchemy 2.0",{"type":7,"value":8,"toc":5358},"minimark",[9,13,32,249,254,259,274,339,536,543,557,561,568,602,615,622,632,636,647,664,748,752,773,780,1158,1167,1171,1175,1193,1204,1718,1728,1732,1933,1936,1999,2003,2007,2019,2026,2107,2113,2150,2154,2299,2303,2562,2566,2577,2588,2600,2604,2608,2611,2632,2724,2809,2902,2906,2921,3055,3063,3067,3082,3242,3252,3340,3344,3363,3599,3615,3619,3623,3629,3671,3841,3844,3850,3884,3888,3891,3956,3962,3966,3976,4077,4084,4211,4215,4222,4311,4317,4321,4328,4370,4383,4387,4396,4410,4498,4502,4505,4799,4804,4828,4832,4835,5023,5026,5030,5176,5180,5189,5215,5238,5258,5284,5310,5314,5354],[10,11,5],"h1",{"id":12},"alembic-async-migrations-and-schema-evolution-for-sqlalchemy-20",[14,15,16,17,22,23,27,28,31],"p",{},"Alembic is the canonical migration tool for SQLAlchemy projects, managing every schema change from initial table creation to complex multi-step zero-downtime alterations — and when your application uses ",[18,19,21],"a",{"href":20},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","async engines and connection pooling",", configuring Alembic correctly to cooperate with ",[24,25,26],"code",{},"asyncio"," requires specific patterns that differ substantially from the synchronous default setup. This guide covers the complete migration lifecycle: wiring Alembic's ",[24,29,30],{},"env.py"," to an async engine, generating and auditing revision scripts, executing zero-downtime schema changes in production, and hardening your migration pipeline for CI\u002FCD deployment.",[33,34,37],"figure",{"className":35},[36],"diagram",[38,39,44,45,44,49,44,44,53,44,44,61,44,69,44,77,44,44,82,44,89,44,44,96,44,99,44,103,44,44,106,44,110,44,44,114,44,116,44,120,44,44,123,44,127,44,44,131,44,135,44,139,44,44,142,44,44,146,44,150,44,155,44,44,159,44,165,44,44,170,44,176,44,180,44,184,44,187,44,190,44,194,44,197,44,200,44,204,44,207,210,44,44,214,44,223,44,227,44,230,44,44,234],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 400","img","Alembic async migration flow from models to version table","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[46,47,48],"title",{},"Alembic Async Migration Flow",[50,51,52],"desc",{},"Diagram showing how SQLAlchemy DeclarativeBase MetaData feeds into Alembic autogenerate, produces a revision script, which runs upgrade or downgrade via an async engine, and updates the alembic_version table.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59,"rx":60},"0","760","400","#f1f9f6","8",[54,62],{"x":63,"y":64,"width":65,"height":66,"rx":67,"fill":68},"24","60","148","64","6","#0f766e",[70,71,76],"text",{"x":72,"y":73,"fill":74,"style":75},"98","87","#ffffff","text-anchor:middle;font-size:13px;font-weight:bold","ORM Models \u002F",[70,78,81],{"x":72,"y":79,"fill":74,"style":80},"105","text-anchor:middle;font-size:13px","DeclarativeBase",[83,84],"line",{"x1":85,"y1":86,"x2":87,"y2":86,"stroke":68,"style":88},"172","92","218","stroke-width:2;marker-end:url(#arr)",[70,90,95],{"x":91,"y":92,"fill":93,"style":94},"195","83","#3f4f4b","text-anchor:middle;font-size:11px","target_metadata",[54,97],{"x":87,"y":64,"width":65,"height":66,"rx":67,"fill":98},"#1f9f95",[70,100,102],{"x":101,"y":73,"fill":74,"style":75},"292","Autogenerate",[70,104,105],{"x":101,"y":79,"fill":74,"style":80},"Diff Engine",[83,107],{"x1":108,"y1":86,"x2":109,"y2":86,"stroke":68,"style":88},"366","412",[70,111,113],{"x":112,"y":92,"fill":93,"style":94},"389","revision script",[54,115],{"x":109,"y":64,"width":65,"height":66,"rx":67,"fill":98},[70,117,119],{"x":118,"y":73,"fill":74,"style":75},"486","Revision Script",[70,121,122],{"x":118,"y":79,"fill":74,"style":80},"upgrade \u002F downgrade",[83,124],{"x1":125,"y1":86,"x2":126,"y2":86,"stroke":68,"style":88},"560","606",[70,128,130],{"x":129,"y":92,"fill":93,"style":94},"583","run_sync",[54,132],{"x":126,"y":64,"width":133,"height":66,"rx":67,"fill":134},"90","#113f39",[70,136,138],{"x":137,"y":73,"fill":74,"style":75},"651","Async",[70,140,141],{"x":137,"y":79,"fill":74,"style":80},"Engine",[83,143],{"x1":137,"y1":144,"x2":137,"y2":145,"stroke":68,"style":88},"124","200",[54,147],{"x":148,"y":145,"width":149,"height":66,"rx":67,"fill":68},"546","160",[70,151,154],{"x":152,"y":153,"fill":74,"style":75},"626","227","alembic_version",[70,156,158],{"x":152,"y":157,"fill":74,"style":80},"245","table (DAG head)",[54,160],{"x":63,"y":145,"width":161,"height":162,"rx":67,"fill":74,"stroke":163,"style":164},"460","140","rgba(15,118,110,0.28)","stroke-width:1.5",[70,166,169],{"x":167,"y":168,"fill":68,"style":75},"254","226","Revision DAG",[171,172],"circle",{"cx":173,"cy":174,"r":175,"fill":98},"80","280","22",[70,177,179],{"x":173,"y":178,"fill":74,"style":94},"285","base",[83,181],{"x1":182,"y1":174,"x2":65,"y2":174,"stroke":68,"style":183},"102","stroke-width:1.5;marker-end:url(#arr)",[171,185],{"cx":186,"cy":174,"r":175,"fill":98},"170",[70,188,189],{"x":186,"y":178,"fill":74,"style":94},"a1b2",[83,191],{"x1":192,"y1":174,"x2":193,"y2":174,"stroke":68,"style":183},"192","238",[171,195],{"cx":196,"cy":174,"r":175,"fill":98},"260",[70,198,199],{"x":196,"y":178,"fill":74,"style":94},"c3d4",[83,201],{"x1":202,"y1":174,"x2":203,"y2":174,"stroke":68,"style":183},"282","328",[171,205],{"cx":206,"cy":174,"r":175,"fill":68},"350",[70,208,209],{"x":206,"y":178,"fill":74,"style":94},"head",[70,211,213],{"x":167,"y":212,"fill":93,"style":94},"322","\neach node = one revision file (upgrade + downgrade)\n",[54,215],{"x":216,"y":217,"width":218,"height":219,"rx":220,"fill":221,"stroke":163,"style":222},"30","356","188","32","4","rgba(15,118,110,0.10)","stroke-width:1",[70,224,226],{"x":144,"y":225,"fill":93,"style":94},"376","Online: live DB connection",[54,228],{"x":229,"y":217,"width":218,"height":219,"rx":220,"fill":221,"stroke":163,"style":222},"244",[70,231,233],{"x":232,"y":225,"fill":93,"style":94},"338","Offline: SQL script output",[235,236,237,238,44],"defs",{},"\n    ",[239,240,244,245,237],"marker",{"id":241,"markerWidth":60,"markerHeight":60,"refX":67,"refY":242,"orient":243},"arr","3","auto","\n      ",[246,247],"path",{"d":248,"fill":68},"M0,0 L0,6 L8,3 z",[250,251,253],"h2",{"id":252},"architectural-foundations","Architectural Foundations",[255,256,258],"h3",{"id":257},"how-alembic-relates-to-sqlalchemy-metadata-and-declarativebase","How Alembic Relates to SQLAlchemy MetaData and DeclarativeBase",[14,260,261,262,266,267,269,270,273],{},"Alembic does not inspect your running application code at migration time — it introspects your ",[263,264,265],"strong",{},"MetaData"," object. In SQLAlchemy 2.0, the preferred pattern is a single ",[24,268,81],{}," subclass whose ",[24,271,272],{},".metadata"," attribute accumulates the complete schema as ORM models are imported:",[275,276,281],"pre",{"className":277,"code":278,"language":279,"meta":280,"style":280},"language-python shiki shiki-themes github-light github-dark","# app\u002Fdb\u002Fbase.py\nfrom sqlalchemy.orm import DeclarativeBase\n\nclass Base(DeclarativeBase):\n    pass\n","python","",[24,282,283,291,308,315,333],{"__ignoreMap":280},[284,285,287],"span",{"class":83,"line":286},1,[284,288,290],{"class":289},"sJ8bj","# app\u002Fdb\u002Fbase.py\n",[284,292,294,298,302,305],{"class":83,"line":293},2,[284,295,297],{"class":296},"szBVR","from",[284,299,301],{"class":300},"sVt8B"," sqlalchemy.orm ",[284,303,304],{"class":296},"import",[284,306,307],{"class":300}," DeclarativeBase\n",[284,309,311],{"class":83,"line":310},3,[284,312,314],{"emptyLinePlaceholder":313},true,"\n",[284,316,318,321,325,328,330],{"class":83,"line":317},4,[284,319,320],{"class":296},"class",[284,322,324],{"class":323},"sScJk"," Base",[284,326,327],{"class":300},"(",[284,329,81],{"class":323},[284,331,332],{"class":300},"):\n",[284,334,336],{"class":83,"line":335},5,[284,337,338],{"class":296},"    pass\n",[275,340,342],{"className":277,"code":341,"language":279,"meta":280,"style":280},"# app\u002Fmodels\u002Fuser.py\nfrom sqlalchemy import String, DateTime, func\nfrom sqlalchemy.orm import Mapped, mapped_column\nfrom app.db.base import Base\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False)\n    created_at: Mapped[DateTime] = mapped_column(\n        DateTime(timezone=True), server_default=func.now()\n    )\n",[24,343,344,349,361,372,384,388,403,416,421,454,496,507,530],{"__ignoreMap":280},[284,345,346],{"class":83,"line":286},[284,347,348],{"class":289},"# app\u002Fmodels\u002Fuser.py\n",[284,350,351,353,356,358],{"class":83,"line":293},[284,352,297],{"class":296},[284,354,355],{"class":300}," sqlalchemy ",[284,357,304],{"class":296},[284,359,360],{"class":300}," String, DateTime, func\n",[284,362,363,365,367,369],{"class":83,"line":310},[284,364,297],{"class":296},[284,366,301],{"class":300},[284,368,304],{"class":296},[284,370,371],{"class":300}," Mapped, mapped_column\n",[284,373,374,376,379,381],{"class":83,"line":317},[284,375,297],{"class":296},[284,377,378],{"class":300}," app.db.base ",[284,380,304],{"class":296},[284,382,383],{"class":300}," Base\n",[284,385,386],{"class":83,"line":335},[284,387,314],{"emptyLinePlaceholder":313},[284,389,391,393,396,398,401],{"class":83,"line":390},6,[284,392,320],{"class":296},[284,394,395],{"class":323}," User",[284,397,327],{"class":300},[284,399,400],{"class":323},"Base",[284,402,332],{"class":300},[284,404,406,409,412],{"class":83,"line":405},7,[284,407,408],{"class":300},"    __tablename__ ",[284,410,411],{"class":296},"=",[284,413,415],{"class":414},"sZZnC"," \"users\"\n",[284,417,419],{"class":83,"line":418},8,[284,420,314],{"emptyLinePlaceholder":313},[284,422,424,428,431,434,437,439,442,446,448,451],{"class":83,"line":423},9,[284,425,427],{"class":426},"sj4cs","    id",[284,429,430],{"class":300},": Mapped[",[284,432,433],{"class":426},"int",[284,435,436],{"class":300},"] ",[284,438,411],{"class":296},[284,440,441],{"class":300}," mapped_column(",[284,443,445],{"class":444},"s4XuR","primary_key",[284,447,411],{"class":296},[284,449,450],{"class":426},"True",[284,452,453],{"class":300},")\n",[284,455,457,460,463,465,467,470,473,476,479,481,483,486,489,491,494],{"class":83,"line":456},10,[284,458,459],{"class":300},"    email: Mapped[",[284,461,462],{"class":426},"str",[284,464,436],{"class":300},[284,466,411],{"class":296},[284,468,469],{"class":300}," mapped_column(String(",[284,471,472],{"class":426},"320",[284,474,475],{"class":300},"), ",[284,477,478],{"class":444},"unique",[284,480,411],{"class":296},[284,482,450],{"class":426},[284,484,485],{"class":300},", ",[284,487,488],{"class":444},"nullable",[284,490,411],{"class":296},[284,492,493],{"class":426},"False",[284,495,453],{"class":300},[284,497,499,502,504],{"class":83,"line":498},11,[284,500,501],{"class":300},"    created_at: Mapped[DateTime] ",[284,503,411],{"class":296},[284,505,506],{"class":300}," mapped_column(\n",[284,508,510,513,516,518,520,522,525,527],{"class":83,"line":509},12,[284,511,512],{"class":300},"        DateTime(",[284,514,515],{"class":444},"timezone",[284,517,411],{"class":296},[284,519,450],{"class":426},[284,521,475],{"class":300},[284,523,524],{"class":444},"server_default",[284,526,411],{"class":296},[284,528,529],{"class":300},"func.now()\n",[284,531,533],{"class":83,"line":532},13,[284,534,535],{"class":300},"    )\n",[14,537,538,539,542],{},"Alembic's autogenerate compares ",[24,540,541],{},"Base.metadata"," (what your Python models declare) against the live database schema (what actually exists) and emits the delta as a revision script. This bi-directional diff is the core value proposition: you express intent in Python, Alembic expresses it in SQL.",[14,544,545,546,548,549,553,554,556],{},"The relationship with ",[24,547,81],{}," is covered in depth in the ",[18,550,552],{"href":551},"\u002Fmastering-sqlalchemy-20-core-and-orm-architecture\u002F","SQLAlchemy 2.0 Core and ORM Architecture guide",", which explains how ",[24,555,265],{}," participates in the ORM's unit-of-work identity map.",[255,558,560],{"id":559},"the-migration-dag-and-revision-identifiers","The Migration DAG and Revision Identifiers",[14,562,563,564,567],{},"Every revision is a node in a ",[263,565,566],{},"directed acyclic graph"," (DAG). Each file contains:",[569,570,571,579,589,596],"ul",{},[572,573,574,575,578],"li",{},"A ",[24,576,577],{},"revision"," identifier — a 12-character hex token generated by Alembic.",[572,580,574,581,584,585,588],{},[24,582,583],{},"down_revision"," pointer — the identifier of the immediately preceding revision (or ",[24,586,587],{},"None"," for the initial revision, or a tuple for merged branches).",[572,590,591,592,595],{},"An ",[24,593,594],{},"upgrade()"," function — the forward migration.",[572,597,574,598,601],{},[24,599,600],{},"downgrade()"," function — the reverse migration.",[14,603,604,605,607,608,611,612,614],{},"Alembic stores the current head revision in the ",[24,606,154],{}," table (one row, one column by default). When you run ",[24,609,610],{},"alembic upgrade head",", Alembic walks the DAG from the current row forward to the latest node and executes each ",[24,613,594],{}," in sequence within individual transactions.",[275,616,620],{"className":617,"code":619,"language":70},[618],"language-text","base ──► a1b2c3 ──► d4e5f6 ──► 7g8h9i  (head)\n                        │\n                        └──► branch_a  (feature branch)\n",[24,621,619],{"__ignoreMap":280},[14,623,624,625,628,629,631],{},"Branch points arise when two developers create revisions concurrently from the same head. You resolve them with ",[24,626,627],{},"alembic merge",", which produces a merge revision that lists both parents in ",[24,630,583],{},".",[255,633,635],{"id":634},"online-vs-offline-mode","Online vs Offline Mode",[14,637,638,641,642,644,645,631],{},[263,639,640],{},"Online mode"," (the default) acquires a live database connection, executes DDL statements directly, and updates ",[24,643,154],{}," atomically. This is what happens in ",[24,646,610],{},[14,648,649,652,653,656,657,659,660,663],{},[263,650,651],{},"Offline mode"," (",[24,654,655],{},"alembic upgrade head --sql",") renders the migration as a SQL script to stdout without touching the database. Production teams at companies with strict change-management processes use offline mode to produce a script that a DBA reviews and runs manually. Offline mode requires that your ",[24,658,30],{}," calls ",[24,661,662],{},"context.configure(literal_binds=True)"," or handles the dialect-specific SQL rendering explicitly.",[275,665,669],{"className":666,"code":667,"language":668,"meta":280,"style":280},"language-bash shiki shiki-themes github-light github-dark","# Generate SQL script for DBA review\nalembic upgrade head --sql > migrations\u002Fupgrade_$(date +%Y%m%d).sql\n\n# Offline downgrade from current head to a specific revision\nalembic downgrade d4e5f6 --sql >> migrations\u002Fdowngrade_$(date +%Y%m%d).sql\n","bash",[24,670,671,676,711,715,720],{"__ignoreMap":280},[284,672,673],{"class":83,"line":286},[284,674,675],{"class":289},"# Generate SQL script for DBA review\n",[284,677,678,681,684,687,690,693,696,699,702,705,708],{"class":83,"line":293},[284,679,680],{"class":323},"alembic",[284,682,683],{"class":414}," upgrade",[284,685,686],{"class":414}," head",[284,688,689],{"class":426}," --sql",[284,691,692],{"class":296}," >",[284,694,695],{"class":414}," migrations\u002Fupgrade_",[284,697,698],{"class":300},"$(",[284,700,701],{"class":323},"date",[284,703,704],{"class":414}," +%Y%m%d",[284,706,707],{"class":300},")",[284,709,710],{"class":414},".sql\n",[284,712,713],{"class":83,"line":310},[284,714,314],{"emptyLinePlaceholder":313},[284,716,717],{"class":83,"line":317},[284,718,719],{"class":289},"# Offline downgrade from current head to a specific revision\n",[284,721,722,724,727,730,732,735,738,740,742,744,746],{"class":83,"line":335},[284,723,680],{"class":323},[284,725,726],{"class":414}," downgrade",[284,728,729],{"class":414}," d4e5f6",[284,731,689],{"class":426},[284,733,734],{"class":296}," >>",[284,736,737],{"class":414}," migrations\u002Fdowngrade_",[284,739,698],{"class":300},[284,741,701],{"class":323},[284,743,704],{"class":414},[284,745,707],{"class":300},[284,747,710],{"class":414},[255,749,751],{"id":750},"data-migrations-vs-schema-migrations","Data Migrations vs Schema Migrations",[14,753,754,755,758,759,762,763,485,766,485,769,772],{},"Alembic is designed for ",[263,756,757],{},"schema migrations"," (DDL), but revision scripts frequently need to also perform ",[263,760,761],{},"data migrations"," (DML — ",[24,764,765],{},"INSERT",[24,767,768],{},"UPDATE",[24,770,771],{},"DELETE",") to transform existing rows as the schema evolves. The key rule is to keep schema operations and data migrations in separate revisions so that each can be rolled back independently.",[14,774,775,776,779],{},"For data migrations, use ",[24,777,778],{},"op.get_bind()"," to access the underlying synchronous connection from within a migration function:",[275,781,783],{"className":277,"code":782,"language":279,"meta":280,"style":280},"from alembic import op\nimport sqlalchemy as sa\n\ndef upgrade() -> None:\n    # Access the synchronous connection — safe inside a run_sync callback\n    bind = op.get_bind()\n\n    # Use Core construct for portable, parameterized DML\n    orders_table = sa.table(\n        \"orders\",\n        sa.column(\"id\", sa.Integer),\n        sa.column(\"status\", sa.String),\n        sa.column(\"legacy_status\", sa.String),\n    )\n\n    # Batch update in chunks of 10 000 to limit lock duration\n    offset = 0\n    batch_size = 10_000\n    while True:\n        result = bind.execute(\n            sa.select(orders_table.c.id, orders_table.c.legacy_status)\n            .where(orders_table.c.status.is_(None))\n            .limit(batch_size)\n            .offset(offset)\n        )\n        rows = result.fetchall()\n        if not rows:\n            break\n        bind.execute(\n            orders_table.update()\n            .where(orders_table.c.id.in_([r.id for r in rows]))\n            .values(status=sa.case(\n                (orders_table.c.legacy_status == \"DONE\", \"fulfilled\"),\n                else_=\"pending\",\n            ))\n        )\n        offset += batch_size\n\n\ndef downgrade() -> None:\n    # Reverting a data migration is often impractical — document why\n    pass\n",[24,784,785,797,809,813,828,833,843,847,852,862,870,881,891,900,905,910,916,927,938,949,960,966,977,983,989,995,1006,1018,1024,1030,1036,1054,1068,1088,1101,1107,1112,1124,1129,1134,1147,1153],{"__ignoreMap":280},[284,786,787,789,792,794],{"class":83,"line":286},[284,788,297],{"class":296},[284,790,791],{"class":300}," alembic ",[284,793,304],{"class":296},[284,795,796],{"class":300}," op\n",[284,798,799,801,803,806],{"class":83,"line":293},[284,800,304],{"class":296},[284,802,355],{"class":300},[284,804,805],{"class":296},"as",[284,807,808],{"class":300}," sa\n",[284,810,811],{"class":83,"line":310},[284,812,314],{"emptyLinePlaceholder":313},[284,814,815,818,820,823,825],{"class":83,"line":317},[284,816,817],{"class":296},"def",[284,819,683],{"class":323},[284,821,822],{"class":300},"() -> ",[284,824,587],{"class":426},[284,826,827],{"class":300},":\n",[284,829,830],{"class":83,"line":335},[284,831,832],{"class":289},"    # Access the synchronous connection — safe inside a run_sync callback\n",[284,834,835,838,840],{"class":83,"line":390},[284,836,837],{"class":300},"    bind ",[284,839,411],{"class":296},[284,841,842],{"class":300}," op.get_bind()\n",[284,844,845],{"class":83,"line":405},[284,846,314],{"emptyLinePlaceholder":313},[284,848,849],{"class":83,"line":418},[284,850,851],{"class":289},"    # Use Core construct for portable, parameterized DML\n",[284,853,854,857,859],{"class":83,"line":423},[284,855,856],{"class":300},"    orders_table ",[284,858,411],{"class":296},[284,860,861],{"class":300}," sa.table(\n",[284,863,864,867],{"class":83,"line":456},[284,865,866],{"class":414},"        \"orders\"",[284,868,869],{"class":300},",\n",[284,871,872,875,878],{"class":83,"line":498},[284,873,874],{"class":300},"        sa.column(",[284,876,877],{"class":414},"\"id\"",[284,879,880],{"class":300},", sa.Integer),\n",[284,882,883,885,888],{"class":83,"line":509},[284,884,874],{"class":300},[284,886,887],{"class":414},"\"status\"",[284,889,890],{"class":300},", sa.String),\n",[284,892,893,895,898],{"class":83,"line":532},[284,894,874],{"class":300},[284,896,897],{"class":414},"\"legacy_status\"",[284,899,890],{"class":300},[284,901,903],{"class":83,"line":902},14,[284,904,535],{"class":300},[284,906,908],{"class":83,"line":907},15,[284,909,314],{"emptyLinePlaceholder":313},[284,911,913],{"class":83,"line":912},16,[284,914,915],{"class":289},"    # Batch update in chunks of 10 000 to limit lock duration\n",[284,917,919,922,924],{"class":83,"line":918},17,[284,920,921],{"class":300},"    offset ",[284,923,411],{"class":296},[284,925,926],{"class":426}," 0\n",[284,928,930,933,935],{"class":83,"line":929},18,[284,931,932],{"class":300},"    batch_size ",[284,934,411],{"class":296},[284,936,937],{"class":426}," 10_000\n",[284,939,941,944,947],{"class":83,"line":940},19,[284,942,943],{"class":296},"    while",[284,945,946],{"class":426}," True",[284,948,827],{"class":300},[284,950,952,955,957],{"class":83,"line":951},20,[284,953,954],{"class":300},"        result ",[284,956,411],{"class":296},[284,958,959],{"class":300}," bind.execute(\n",[284,961,963],{"class":83,"line":962},21,[284,964,965],{"class":300},"            sa.select(orders_table.c.id, orders_table.c.legacy_status)\n",[284,967,969,972,974],{"class":83,"line":968},22,[284,970,971],{"class":300},"            .where(orders_table.c.status.is_(",[284,973,587],{"class":426},[284,975,976],{"class":300},"))\n",[284,978,980],{"class":83,"line":979},23,[284,981,982],{"class":300},"            .limit(batch_size)\n",[284,984,986],{"class":83,"line":985},24,[284,987,988],{"class":300},"            .offset(offset)\n",[284,990,992],{"class":83,"line":991},25,[284,993,994],{"class":300},"        )\n",[284,996,998,1001,1003],{"class":83,"line":997},26,[284,999,1000],{"class":300},"        rows ",[284,1002,411],{"class":296},[284,1004,1005],{"class":300}," result.fetchall()\n",[284,1007,1009,1012,1015],{"class":83,"line":1008},27,[284,1010,1011],{"class":296},"        if",[284,1013,1014],{"class":296}," not",[284,1016,1017],{"class":300}," rows:\n",[284,1019,1021],{"class":83,"line":1020},28,[284,1022,1023],{"class":296},"            break\n",[284,1025,1027],{"class":83,"line":1026},29,[284,1028,1029],{"class":300},"        bind.execute(\n",[284,1031,1033],{"class":83,"line":1032},30,[284,1034,1035],{"class":300},"            orders_table.update()\n",[284,1037,1039,1042,1045,1048,1051],{"class":83,"line":1038},31,[284,1040,1041],{"class":300},"            .where(orders_table.c.id.in_([r.id ",[284,1043,1044],{"class":296},"for",[284,1046,1047],{"class":300}," r ",[284,1049,1050],{"class":296},"in",[284,1052,1053],{"class":300}," rows]))\n",[284,1055,1057,1060,1063,1065],{"class":83,"line":1056},32,[284,1058,1059],{"class":300},"            .values(",[284,1061,1062],{"class":444},"status",[284,1064,411],{"class":296},[284,1066,1067],{"class":300},"sa.case(\n",[284,1069,1071,1074,1077,1080,1082,1085],{"class":83,"line":1070},33,[284,1072,1073],{"class":300},"                (orders_table.c.legacy_status ",[284,1075,1076],{"class":296},"==",[284,1078,1079],{"class":414}," \"DONE\"",[284,1081,485],{"class":300},[284,1083,1084],{"class":414},"\"fulfilled\"",[284,1086,1087],{"class":300},"),\n",[284,1089,1091,1094,1096,1099],{"class":83,"line":1090},34,[284,1092,1093],{"class":444},"                else_",[284,1095,411],{"class":296},[284,1097,1098],{"class":414},"\"pending\"",[284,1100,869],{"class":300},[284,1102,1104],{"class":83,"line":1103},35,[284,1105,1106],{"class":300},"            ))\n",[284,1108,1110],{"class":83,"line":1109},36,[284,1111,994],{"class":300},[284,1113,1115,1118,1121],{"class":83,"line":1114},37,[284,1116,1117],{"class":300},"        offset ",[284,1119,1120],{"class":296},"+=",[284,1122,1123],{"class":300}," batch_size\n",[284,1125,1127],{"class":83,"line":1126},38,[284,1128,314],{"emptyLinePlaceholder":313},[284,1130,1132],{"class":83,"line":1131},39,[284,1133,314],{"emptyLinePlaceholder":313},[284,1135,1137,1139,1141,1143,1145],{"class":83,"line":1136},40,[284,1138,817],{"class":296},[284,1140,726],{"class":323},[284,1142,822],{"class":300},[284,1144,587],{"class":426},[284,1146,827],{"class":300},[284,1148,1150],{"class":83,"line":1149},41,[284,1151,1152],{"class":289},"    # Reverting a data migration is often impractical — document why\n",[284,1154,1156],{"class":83,"line":1155},42,[284,1157,338],{"class":296},[14,1159,1160,1161,1163,1164,1166],{},"Avoid using ORM session objects inside migration scripts. The ",[24,1162,778],{}," pattern works with raw Core expressions and remains safe across all async\u002Fsync engine configurations because ",[24,1165,130],{}," provides the synchronous DBAPI connection.",[250,1168,1170],{"id":1169},"key-component-deep-dive-1-configuring-alembic-for-an-async-engine","Key Component Deep-Dive 1: Configuring Alembic for an Async Engine",[255,1172,1174],{"id":1173},"the-envpy-challenge","The env.py Challenge",[14,1176,1177,1178,1180,1181,1184,1185,1188,1189,631],{},"Alembic's default ",[24,1179,30],{}," template uses synchronous SQLAlchemy APIs. When your application uses ",[24,1182,1183],{},"create_async_engine",", you must bridge the synchronous migration runner to the async driver using ",[24,1186,1187],{},"connection.run_sync",". This is the single most common stumbling block when ",[18,1190,1192],{"href":1191},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002F","configuring Alembic with async SQLAlchemy engines",[14,1194,1195,1196,1199,1200,1203],{},"The key insight is that Alembic's internal migration logic (",[24,1197,1198],{},"context.run_migrations()",") is synchronous. You must hand it a synchronous DBAPI connection obtained by unwrapping the async engine's underlying connection. SQLAlchemy provides ",[24,1201,1202],{},"AsyncConnection.run_sync()"," for exactly this purpose.",[275,1205,1207],{"className":277,"code":1206,"language":279,"meta":280,"style":280},"# alembic\u002Fenv.py — full async-compatible implementation\nimport asyncio\nfrom logging.config import fileConfig\n\nfrom alembic import context\nfrom sqlalchemy import pool\nfrom sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine\n\n# Import your Base so MetaData is populated\nfrom app.db.base import Base\nfrom app.models import user, order, product  # noqa: F401 — triggers __tablename__ registration\n\nconfig = context.config\nfileConfig(config.config_file_name)\n\ntarget_metadata = Base.metadata\n\n\ndef run_migrations_offline() -> None:\n    \"\"\"Run migrations in 'offline' mode — renders SQL without a live connection.\"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True,\n        dialect_opts={\"paramstyle\": \"named\"},\n        compare_type=True,\n        compare_server_default=True,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef do_run_migrations(connection) -> None:\n    \"\"\"Synchronous callback executed inside run_sync.\"\"\"\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        compare_type=True,\n        compare_server_default=True,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n\n\nasync def run_migrations_online() -> None:\n    \"\"\"Run migrations in 'online' mode against a live async engine.\"\"\"\n    connectable: AsyncEngine = create_async_engine(\n        config.get_main_option(\"sqlalchemy.url\"),\n        poolclass=pool.NullPool,  # NullPool: migrations are single-use; no persistent pool needed\n    )\n\n    async with connectable.connect() as connection:\n        await connection.run_sync(do_run_migrations)\n\n    await connectable.dispose()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    asyncio.run(run_migrations_online())\n",[24,1208,1209,1214,1221,1233,1237,1248,1259,1271,1275,1280,1290,1305,1309,1319,1324,1328,1338,1342,1346,1359,1364,1379,1384,1394,1404,1415,1437,1448,1459,1463,1471,1476,1480,1484,1498,1503,1507,1517,1525,1535,1545,1549,1555,1560,1565,1570,1588,1594,1605,1615,1629,1634,1639,1656,1665,1670,1679,1684,1689,1698,1704,1712],{"__ignoreMap":280},[284,1210,1211],{"class":83,"line":286},[284,1212,1213],{"class":289},"# alembic\u002Fenv.py — full async-compatible implementation\n",[284,1215,1216,1218],{"class":83,"line":293},[284,1217,304],{"class":296},[284,1219,1220],{"class":300}," asyncio\n",[284,1222,1223,1225,1228,1230],{"class":83,"line":310},[284,1224,297],{"class":296},[284,1226,1227],{"class":300}," logging.config ",[284,1229,304],{"class":296},[284,1231,1232],{"class":300}," fileConfig\n",[284,1234,1235],{"class":83,"line":317},[284,1236,314],{"emptyLinePlaceholder":313},[284,1238,1239,1241,1243,1245],{"class":83,"line":335},[284,1240,297],{"class":296},[284,1242,791],{"class":300},[284,1244,304],{"class":296},[284,1246,1247],{"class":300}," context\n",[284,1249,1250,1252,1254,1256],{"class":83,"line":390},[284,1251,297],{"class":296},[284,1253,355],{"class":300},[284,1255,304],{"class":296},[284,1257,1258],{"class":300}," pool\n",[284,1260,1261,1263,1266,1268],{"class":83,"line":405},[284,1262,297],{"class":296},[284,1264,1265],{"class":300}," sqlalchemy.ext.asyncio ",[284,1267,304],{"class":296},[284,1269,1270],{"class":300}," AsyncEngine, create_async_engine\n",[284,1272,1273],{"class":83,"line":418},[284,1274,314],{"emptyLinePlaceholder":313},[284,1276,1277],{"class":83,"line":423},[284,1278,1279],{"class":289},"# Import your Base so MetaData is populated\n",[284,1281,1282,1284,1286,1288],{"class":83,"line":456},[284,1283,297],{"class":296},[284,1285,378],{"class":300},[284,1287,304],{"class":296},[284,1289,383],{"class":300},[284,1291,1292,1294,1297,1299,1302],{"class":83,"line":498},[284,1293,297],{"class":296},[284,1295,1296],{"class":300}," app.models ",[284,1298,304],{"class":296},[284,1300,1301],{"class":300}," user, order, product  ",[284,1303,1304],{"class":289},"# noqa: F401 — triggers __tablename__ registration\n",[284,1306,1307],{"class":83,"line":509},[284,1308,314],{"emptyLinePlaceholder":313},[284,1310,1311,1314,1316],{"class":83,"line":532},[284,1312,1313],{"class":300},"config ",[284,1315,411],{"class":296},[284,1317,1318],{"class":300}," context.config\n",[284,1320,1321],{"class":83,"line":902},[284,1322,1323],{"class":300},"fileConfig(config.config_file_name)\n",[284,1325,1326],{"class":83,"line":907},[284,1327,314],{"emptyLinePlaceholder":313},[284,1329,1330,1333,1335],{"class":83,"line":912},[284,1331,1332],{"class":300},"target_metadata ",[284,1334,411],{"class":296},[284,1336,1337],{"class":300}," Base.metadata\n",[284,1339,1340],{"class":83,"line":918},[284,1341,314],{"emptyLinePlaceholder":313},[284,1343,1344],{"class":83,"line":929},[284,1345,314],{"emptyLinePlaceholder":313},[284,1347,1348,1350,1353,1355,1357],{"class":83,"line":940},[284,1349,817],{"class":296},[284,1351,1352],{"class":323}," run_migrations_offline",[284,1354,822],{"class":300},[284,1356,587],{"class":426},[284,1358,827],{"class":300},[284,1360,1361],{"class":83,"line":951},[284,1362,1363],{"class":414},"    \"\"\"Run migrations in 'offline' mode — renders SQL without a live connection.\"\"\"\n",[284,1365,1366,1369,1371,1374,1377],{"class":83,"line":962},[284,1367,1368],{"class":300},"    url ",[284,1370,411],{"class":296},[284,1372,1373],{"class":300}," config.get_main_option(",[284,1375,1376],{"class":414},"\"sqlalchemy.url\"",[284,1378,453],{"class":300},[284,1380,1381],{"class":83,"line":968},[284,1382,1383],{"class":300},"    context.configure(\n",[284,1385,1386,1389,1391],{"class":83,"line":979},[284,1387,1388],{"class":444},"        url",[284,1390,411],{"class":296},[284,1392,1393],{"class":300},"url,\n",[284,1395,1396,1399,1401],{"class":83,"line":985},[284,1397,1398],{"class":444},"        target_metadata",[284,1400,411],{"class":296},[284,1402,1403],{"class":300},"target_metadata,\n",[284,1405,1406,1409,1411,1413],{"class":83,"line":991},[284,1407,1408],{"class":444},"        literal_binds",[284,1410,411],{"class":296},[284,1412,450],{"class":426},[284,1414,869],{"class":300},[284,1416,1417,1420,1422,1425,1428,1431,1434],{"class":83,"line":997},[284,1418,1419],{"class":444},"        dialect_opts",[284,1421,411],{"class":296},[284,1423,1424],{"class":300},"{",[284,1426,1427],{"class":414},"\"paramstyle\"",[284,1429,1430],{"class":300},": ",[284,1432,1433],{"class":414},"\"named\"",[284,1435,1436],{"class":300},"},\n",[284,1438,1439,1442,1444,1446],{"class":83,"line":1008},[284,1440,1441],{"class":444},"        compare_type",[284,1443,411],{"class":296},[284,1445,450],{"class":426},[284,1447,869],{"class":300},[284,1449,1450,1453,1455,1457],{"class":83,"line":1020},[284,1451,1452],{"class":444},"        compare_server_default",[284,1454,411],{"class":296},[284,1456,450],{"class":426},[284,1458,869],{"class":300},[284,1460,1461],{"class":83,"line":1026},[284,1462,535],{"class":300},[284,1464,1465,1468],{"class":83,"line":1032},[284,1466,1467],{"class":296},"    with",[284,1469,1470],{"class":300}," context.begin_transaction():\n",[284,1472,1473],{"class":83,"line":1038},[284,1474,1475],{"class":300},"        context.run_migrations()\n",[284,1477,1478],{"class":83,"line":1056},[284,1479,314],{"emptyLinePlaceholder":313},[284,1481,1482],{"class":83,"line":1070},[284,1483,314],{"emptyLinePlaceholder":313},[284,1485,1486,1488,1491,1494,1496],{"class":83,"line":1090},[284,1487,817],{"class":296},[284,1489,1490],{"class":323}," do_run_migrations",[284,1492,1493],{"class":300},"(connection) -> ",[284,1495,587],{"class":426},[284,1497,827],{"class":300},[284,1499,1500],{"class":83,"line":1103},[284,1501,1502],{"class":414},"    \"\"\"Synchronous callback executed inside run_sync.\"\"\"\n",[284,1504,1505],{"class":83,"line":1109},[284,1506,1383],{"class":300},[284,1508,1509,1512,1514],{"class":83,"line":1114},[284,1510,1511],{"class":444},"        connection",[284,1513,411],{"class":296},[284,1515,1516],{"class":300},"connection,\n",[284,1518,1519,1521,1523],{"class":83,"line":1126},[284,1520,1398],{"class":444},[284,1522,411],{"class":296},[284,1524,1403],{"class":300},[284,1526,1527,1529,1531,1533],{"class":83,"line":1131},[284,1528,1441],{"class":444},[284,1530,411],{"class":296},[284,1532,450],{"class":426},[284,1534,869],{"class":300},[284,1536,1537,1539,1541,1543],{"class":83,"line":1136},[284,1538,1452],{"class":444},[284,1540,411],{"class":296},[284,1542,450],{"class":426},[284,1544,869],{"class":300},[284,1546,1547],{"class":83,"line":1149},[284,1548,535],{"class":300},[284,1550,1551,1553],{"class":83,"line":1155},[284,1552,1467],{"class":296},[284,1554,1470],{"class":300},[284,1556,1558],{"class":83,"line":1557},43,[284,1559,1475],{"class":300},[284,1561,1563],{"class":83,"line":1562},44,[284,1564,314],{"emptyLinePlaceholder":313},[284,1566,1568],{"class":83,"line":1567},45,[284,1569,314],{"emptyLinePlaceholder":313},[284,1571,1573,1576,1579,1582,1584,1586],{"class":83,"line":1572},46,[284,1574,1575],{"class":296},"async",[284,1577,1578],{"class":296}," def",[284,1580,1581],{"class":323}," run_migrations_online",[284,1583,822],{"class":300},[284,1585,587],{"class":426},[284,1587,827],{"class":300},[284,1589,1591],{"class":83,"line":1590},47,[284,1592,1593],{"class":414},"    \"\"\"Run migrations in 'online' mode against a live async engine.\"\"\"\n",[284,1595,1597,1600,1602],{"class":83,"line":1596},48,[284,1598,1599],{"class":300},"    connectable: AsyncEngine ",[284,1601,411],{"class":296},[284,1603,1604],{"class":300}," create_async_engine(\n",[284,1606,1608,1611,1613],{"class":83,"line":1607},49,[284,1609,1610],{"class":300},"        config.get_main_option(",[284,1612,1376],{"class":414},[284,1614,1087],{"class":300},[284,1616,1618,1621,1623,1626],{"class":83,"line":1617},50,[284,1619,1620],{"class":444},"        poolclass",[284,1622,411],{"class":296},[284,1624,1625],{"class":300},"pool.NullPool,  ",[284,1627,1628],{"class":289},"# NullPool: migrations are single-use; no persistent pool needed\n",[284,1630,1632],{"class":83,"line":1631},51,[284,1633,535],{"class":300},[284,1635,1637],{"class":83,"line":1636},52,[284,1638,314],{"emptyLinePlaceholder":313},[284,1640,1642,1645,1648,1651,1653],{"class":83,"line":1641},53,[284,1643,1644],{"class":296},"    async",[284,1646,1647],{"class":296}," with",[284,1649,1650],{"class":300}," connectable.connect() ",[284,1652,805],{"class":296},[284,1654,1655],{"class":300}," connection:\n",[284,1657,1659,1662],{"class":83,"line":1658},54,[284,1660,1661],{"class":296},"        await",[284,1663,1664],{"class":300}," connection.run_sync(do_run_migrations)\n",[284,1666,1668],{"class":83,"line":1667},55,[284,1669,314],{"emptyLinePlaceholder":313},[284,1671,1673,1676],{"class":83,"line":1672},56,[284,1674,1675],{"class":296},"    await",[284,1677,1678],{"class":300}," connectable.dispose()\n",[284,1680,1682],{"class":83,"line":1681},57,[284,1683,314],{"emptyLinePlaceholder":313},[284,1685,1687],{"class":83,"line":1686},58,[284,1688,314],{"emptyLinePlaceholder":313},[284,1690,1692,1695],{"class":83,"line":1691},59,[284,1693,1694],{"class":296},"if",[284,1696,1697],{"class":300}," context.is_offline_mode():\n",[284,1699,1701],{"class":83,"line":1700},60,[284,1702,1703],{"class":300},"    run_migrations_offline()\n",[284,1705,1707,1710],{"class":83,"line":1706},61,[284,1708,1709],{"class":296},"else",[284,1711,827],{"class":300},[284,1713,1715],{"class":83,"line":1714},62,[284,1716,1717],{"class":300},"    asyncio.run(run_migrations_online())\n",[14,1719,1720,1721,1724,1725,1727],{},"The ",[24,1722,1723],{},"NullPool"," is deliberate: migration processes are typically short-lived scripts, not long-running servers. Using ",[24,1726,1723],{}," guarantees that the connection is physically closed after the migration completes rather than being returned to a pool that will be immediately discarded.",[255,1729,1731],{"id":1730},"alembicini-configuration","alembic.ini Configuration",[275,1733,1737],{"className":1734,"code":1735,"language":1736,"meta":280,"style":280},"language-ini shiki shiki-themes github-light github-dark","# alembic.ini (relevant sections)\n[alembic]\nscript_location = alembic\nprepend_sys_path = .\n\n# Use an environment variable so secrets never live in source control\nsqlalchemy.url = postgresql+asyncpg:\u002F\u002F%(DB_USER)s:%(DB_PASS)s@%(DB_HOST)s\u002F%(DB_NAME)s\n\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n","ini",[24,1738,1739,1744,1749,1754,1759,1763,1768,1773,1777,1782,1787,1791,1796,1801,1805,1810,1815,1819,1824,1829,1834,1839,1843,1848,1852,1857,1862,1866,1871,1876,1880,1885,1889,1894,1899,1904,1909,1914,1918,1923,1928],{"__ignoreMap":280},[284,1740,1741],{"class":83,"line":286},[284,1742,1743],{},"# alembic.ini (relevant sections)\n",[284,1745,1746],{"class":83,"line":293},[284,1747,1748],{},"[alembic]\n",[284,1750,1751],{"class":83,"line":310},[284,1752,1753],{},"script_location = alembic\n",[284,1755,1756],{"class":83,"line":317},[284,1757,1758],{},"prepend_sys_path = .\n",[284,1760,1761],{"class":83,"line":335},[284,1762,314],{"emptyLinePlaceholder":313},[284,1764,1765],{"class":83,"line":390},[284,1766,1767],{},"# Use an environment variable so secrets never live in source control\n",[284,1769,1770],{"class":83,"line":405},[284,1771,1772],{},"sqlalchemy.url = postgresql+asyncpg:\u002F\u002F%(DB_USER)s:%(DB_PASS)s@%(DB_HOST)s\u002F%(DB_NAME)s\n",[284,1774,1775],{"class":83,"line":418},[284,1776,314],{"emptyLinePlaceholder":313},[284,1778,1779],{"class":83,"line":423},[284,1780,1781],{},"[loggers]\n",[284,1783,1784],{"class":83,"line":456},[284,1785,1786],{},"keys = root,sqlalchemy,alembic\n",[284,1788,1789],{"class":83,"line":498},[284,1790,314],{"emptyLinePlaceholder":313},[284,1792,1793],{"class":83,"line":509},[284,1794,1795],{},"[handlers]\n",[284,1797,1798],{"class":83,"line":532},[284,1799,1800],{},"keys = console\n",[284,1802,1803],{"class":83,"line":902},[284,1804,314],{"emptyLinePlaceholder":313},[284,1806,1807],{"class":83,"line":907},[284,1808,1809],{},"[formatters]\n",[284,1811,1812],{"class":83,"line":912},[284,1813,1814],{},"keys = generic\n",[284,1816,1817],{"class":83,"line":918},[284,1818,314],{"emptyLinePlaceholder":313},[284,1820,1821],{"class":83,"line":929},[284,1822,1823],{},"[logger_root]\n",[284,1825,1826],{"class":83,"line":940},[284,1827,1828],{},"level = WARN\n",[284,1830,1831],{"class":83,"line":951},[284,1832,1833],{},"handlers = console\n",[284,1835,1836],{"class":83,"line":962},[284,1837,1838],{},"qualname =\n",[284,1840,1841],{"class":83,"line":968},[284,1842,314],{"emptyLinePlaceholder":313},[284,1844,1845],{"class":83,"line":979},[284,1846,1847],{},"[logger_sqlalchemy]\n",[284,1849,1850],{"class":83,"line":985},[284,1851,1828],{},[284,1853,1854],{"class":83,"line":991},[284,1855,1856],{},"handlers =\n",[284,1858,1859],{"class":83,"line":997},[284,1860,1861],{},"qualname = sqlalchemy.engine\n",[284,1863,1864],{"class":83,"line":1008},[284,1865,314],{"emptyLinePlaceholder":313},[284,1867,1868],{"class":83,"line":1020},[284,1869,1870],{},"[logger_alembic]\n",[284,1872,1873],{"class":83,"line":1026},[284,1874,1875],{},"level = INFO\n",[284,1877,1878],{"class":83,"line":1032},[284,1879,1856],{},[284,1881,1882],{"class":83,"line":1038},[284,1883,1884],{},"qualname = alembic\n",[284,1886,1887],{"class":83,"line":1056},[284,1888,314],{"emptyLinePlaceholder":313},[284,1890,1891],{"class":83,"line":1070},[284,1892,1893],{},"[handler_console]\n",[284,1895,1896],{"class":83,"line":1090},[284,1897,1898],{},"class = StreamHandler\n",[284,1900,1901],{"class":83,"line":1103},[284,1902,1903],{},"args = (sys.stderr,)\n",[284,1905,1906],{"class":83,"line":1109},[284,1907,1908],{},"level = NOTSET\n",[284,1910,1911],{"class":83,"line":1114},[284,1912,1913],{},"formatter = generic\n",[284,1915,1916],{"class":83,"line":1126},[284,1917,314],{"emptyLinePlaceholder":313},[284,1919,1920],{"class":83,"line":1131},[284,1921,1922],{},"[formatter_generic]\n",[284,1924,1925],{"class":83,"line":1136},[284,1926,1927],{},"format = %(levelname)-5.5s [%(name)s] %(message)s\n",[284,1929,1930],{"class":83,"line":1149},[284,1931,1932],{},"datefmt = %H:%M:%S\n",[14,1934,1935],{},"Inject credentials at runtime via environment variables or a secrets manager:",[275,1937,1939],{"className":666,"code":1938,"language":668,"meta":280,"style":280},"export DB_USER=app_user\nexport DB_PASS=s3cur3p4ss\nexport DB_HOST=db.internal\nexport DB_NAME=app_production\nalembic upgrade head\n",[24,1940,1941,1954,1966,1978,1990],{"__ignoreMap":280},[284,1942,1943,1946,1949,1951],{"class":83,"line":286},[284,1944,1945],{"class":296},"export",[284,1947,1948],{"class":300}," DB_USER",[284,1950,411],{"class":296},[284,1952,1953],{"class":300},"app_user\n",[284,1955,1956,1958,1961,1963],{"class":83,"line":293},[284,1957,1945],{"class":296},[284,1959,1960],{"class":300}," DB_PASS",[284,1962,411],{"class":296},[284,1964,1965],{"class":300},"s3cur3p4ss\n",[284,1967,1968,1970,1973,1975],{"class":83,"line":310},[284,1969,1945],{"class":296},[284,1971,1972],{"class":300}," DB_HOST",[284,1974,411],{"class":296},[284,1976,1977],{"class":300},"db.internal\n",[284,1979,1980,1982,1985,1987],{"class":83,"line":317},[284,1981,1945],{"class":296},[284,1983,1984],{"class":300}," DB_NAME",[284,1986,411],{"class":296},[284,1988,1989],{"class":300},"app_production\n",[284,1991,1992,1994,1996],{"class":83,"line":335},[284,1993,680],{"class":323},[284,1995,683],{"class":414},[284,1997,1998],{"class":414}," head\n",[250,2000,2002],{"id":2001},"key-component-deep-dive-2-autogenerate-workflow-and-reviewing-revisions","Key Component Deep-Dive 2: Autogenerate Workflow and Reviewing Revisions",[255,2004,2006],{"id":2005},"setting-up-target_metadata-correctly","Setting Up target_metadata Correctly",[14,2008,2009,2011,2012,2014,2015,2018],{},[24,2010,95],{}," must reflect your complete schema at import time. Any model file that is not imported before ",[24,2013,30],{}," runs will be invisible to the autogenerate diff, causing Alembic to think those tables do not exist in your models and scheduling them for ",[24,2016,2017],{},"DROP TABLE",". This is a source of data-loss incidents.",[14,2020,2021,2022,2025],{},"The safest pattern is a dedicated ",[24,2023,2024],{},"app\u002Fmodels\u002F__init__.py"," that imports every model:",[275,2027,2029],{"className":277,"code":2028,"language":279,"meta":280,"style":280},"# app\u002Fmodels\u002F__init__.py\nfrom app.models.user import User       # noqa: F401\nfrom app.models.order import Order     # noqa: F401\nfrom app.models.product import Product # noqa: F401\nfrom app.models.invoice import Invoice # noqa: F401\nfrom app.models.tenant import Tenant   # noqa: F401\n",[24,2030,2031,2036,2051,2065,2079,2093],{"__ignoreMap":280},[284,2032,2033],{"class":83,"line":286},[284,2034,2035],{"class":289},"# app\u002Fmodels\u002F__init__.py\n",[284,2037,2038,2040,2043,2045,2048],{"class":83,"line":293},[284,2039,297],{"class":296},[284,2041,2042],{"class":300}," app.models.user ",[284,2044,304],{"class":296},[284,2046,2047],{"class":300}," User       ",[284,2049,2050],{"class":289},"# noqa: F401\n",[284,2052,2053,2055,2058,2060,2063],{"class":83,"line":310},[284,2054,297],{"class":296},[284,2056,2057],{"class":300}," app.models.order ",[284,2059,304],{"class":296},[284,2061,2062],{"class":300}," Order     ",[284,2064,2050],{"class":289},[284,2066,2067,2069,2072,2074,2077],{"class":83,"line":317},[284,2068,297],{"class":296},[284,2070,2071],{"class":300}," app.models.product ",[284,2073,304],{"class":296},[284,2075,2076],{"class":300}," Product ",[284,2078,2050],{"class":289},[284,2080,2081,2083,2086,2088,2091],{"class":83,"line":335},[284,2082,297],{"class":296},[284,2084,2085],{"class":300}," app.models.invoice ",[284,2087,304],{"class":296},[284,2089,2090],{"class":300}," Invoice ",[284,2092,2050],{"class":289},[284,2094,2095,2097,2100,2102,2105],{"class":83,"line":390},[284,2096,297],{"class":296},[284,2098,2099],{"class":300}," app.models.tenant ",[284,2101,304],{"class":296},[284,2103,2104],{"class":300}," Tenant   ",[284,2106,2050],{"class":289},[14,2108,2109,2110,2112],{},"Then in ",[24,2111,30],{},":",[275,2114,2116],{"className":277,"code":2115,"language":279,"meta":280,"style":280},"import app.models  # noqa: F401 — side-effect: all __tablename__ registrations fire\nfrom app.db.base import Base\n\ntarget_metadata = Base.metadata\n",[24,2117,2118,2128,2138,2142],{"__ignoreMap":280},[284,2119,2120,2122,2125],{"class":83,"line":286},[284,2121,304],{"class":296},[284,2123,2124],{"class":300}," app.models  ",[284,2126,2127],{"class":289},"# noqa: F401 — side-effect: all __tablename__ registrations fire\n",[284,2129,2130,2132,2134,2136],{"class":83,"line":293},[284,2131,297],{"class":296},[284,2133,378],{"class":300},[284,2135,304],{"class":296},[284,2137,383],{"class":300},[284,2139,2140],{"class":83,"line":310},[284,2141,314],{"emptyLinePlaceholder":313},[284,2143,2144,2146,2148],{"class":83,"line":317},[284,2145,1332],{"class":300},[284,2147,411],{"class":296},[284,2149,1337],{"class":300},[255,2151,2153],{"id":2152},"generating-and-running-a-revision","Generating and Running a Revision",[275,2155,2157],{"className":666,"code":2156,"language":668,"meta":280,"style":280},"# Generate an autogenerated revision\nalembic revision --autogenerate -m \"add_invoice_due_date\"\n\n# Inspect what was generated\ncat alembic\u002Fversions\u002F\u003Ctimestamp>_add_invoice_due_date.py\n\n# Apply to development database\nalembic upgrade head\n\n# Apply only the next revision (useful for incremental testing)\nalembic upgrade +1\n\n# Roll back one revision\nalembic downgrade -1\n\n# Show current database revision\nalembic current\n\n# Show full revision history\nalembic history --verbose\n",[24,2158,2159,2164,2180,2184,2189,2211,2215,2220,2228,2232,2237,2246,2250,2255,2264,2268,2273,2280,2284,2289],{"__ignoreMap":280},[284,2160,2161],{"class":83,"line":286},[284,2162,2163],{"class":289},"# Generate an autogenerated revision\n",[284,2165,2166,2168,2171,2174,2177],{"class":83,"line":293},[284,2167,680],{"class":323},[284,2169,2170],{"class":414}," revision",[284,2172,2173],{"class":426}," --autogenerate",[284,2175,2176],{"class":426}," -m",[284,2178,2179],{"class":414}," \"add_invoice_due_date\"\n",[284,2181,2182],{"class":83,"line":310},[284,2183,314],{"emptyLinePlaceholder":313},[284,2185,2186],{"class":83,"line":317},[284,2187,2188],{"class":289},"# Inspect what was generated\n",[284,2190,2191,2194,2197,2200,2203,2205,2208],{"class":83,"line":335},[284,2192,2193],{"class":323},"cat",[284,2195,2196],{"class":414}," alembic\u002Fversions\u002F",[284,2198,2199],{"class":296},"\u003C",[284,2201,2202],{"class":414},"timestam",[284,2204,14],{"class":300},[284,2206,2207],{"class":296},">",[284,2209,2210],{"class":414},"_add_invoice_due_date.py\n",[284,2212,2213],{"class":83,"line":390},[284,2214,314],{"emptyLinePlaceholder":313},[284,2216,2217],{"class":83,"line":405},[284,2218,2219],{"class":289},"# Apply to development database\n",[284,2221,2222,2224,2226],{"class":83,"line":418},[284,2223,680],{"class":323},[284,2225,683],{"class":414},[284,2227,1998],{"class":414},[284,2229,2230],{"class":83,"line":423},[284,2231,314],{"emptyLinePlaceholder":313},[284,2233,2234],{"class":83,"line":456},[284,2235,2236],{"class":289},"# Apply only the next revision (useful for incremental testing)\n",[284,2238,2239,2241,2243],{"class":83,"line":498},[284,2240,680],{"class":323},[284,2242,683],{"class":414},[284,2244,2245],{"class":414}," +1\n",[284,2247,2248],{"class":83,"line":509},[284,2249,314],{"emptyLinePlaceholder":313},[284,2251,2252],{"class":83,"line":532},[284,2253,2254],{"class":289},"# Roll back one revision\n",[284,2256,2257,2259,2261],{"class":83,"line":902},[284,2258,680],{"class":323},[284,2260,726],{"class":414},[284,2262,2263],{"class":426}," -1\n",[284,2265,2266],{"class":83,"line":907},[284,2267,314],{"emptyLinePlaceholder":313},[284,2269,2270],{"class":83,"line":912},[284,2271,2272],{"class":289},"# Show current database revision\n",[284,2274,2275,2277],{"class":83,"line":918},[284,2276,680],{"class":323},[284,2278,2279],{"class":414}," current\n",[284,2281,2282],{"class":83,"line":929},[284,2283,314],{"emptyLinePlaceholder":313},[284,2285,2286],{"class":83,"line":940},[284,2287,2288],{"class":289},"# Show full revision history\n",[284,2290,2291,2293,2296],{"class":83,"line":951},[284,2292,680],{"class":323},[284,2294,2295],{"class":414}," history",[284,2297,2298],{"class":426}," --verbose\n",[255,2300,2302],{"id":2301},"reading-a-generated-revision","Reading a Generated Revision",[275,2304,2306],{"className":277,"code":2305,"language":279,"meta":280,"style":280},"# alembic\u002Fversions\u002F20260618_001_add_invoice_due_date.py\n\"\"\"add invoice due_date\n\nRevision ID: 3f7a1c8b2e49\nRevises: 9d2e5a4b1c73\nCreate Date: 2026-06-18 09:14:32.887123\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision: str = \"3f7a1c8b2e49\"\ndown_revision: str | tuple = \"9d2e5a4b1c73\"\nbranch_labels: str | tuple | None = None\ndepends_on: str | tuple | None = None\n\n\ndef upgrade() -> None:\n    op.add_column(\n        \"invoices\",\n        sa.Column(\n            \"due_date\",\n            sa.Date(),\n            nullable=True,  # always nullable first — see zero-downtime section\n        ),\n    )\n\n\ndef downgrade() -> None:\n    op.drop_column(\"invoices\", \"due_date\")\n",[24,2307,2308,2313,2318,2322,2327,2332,2337,2342,2355,2359,2369,2379,2383,2396,2414,2435,2454,2458,2462,2474,2479,2486,2491,2498,2503,2518,2523,2527,2531,2535,2547],{"__ignoreMap":280},[284,2309,2310],{"class":83,"line":286},[284,2311,2312],{"class":289},"# alembic\u002Fversions\u002F20260618_001_add_invoice_due_date.py\n",[284,2314,2315],{"class":83,"line":293},[284,2316,2317],{"class":414},"\"\"\"add invoice due_date\n",[284,2319,2320],{"class":83,"line":310},[284,2321,314],{"emptyLinePlaceholder":313},[284,2323,2324],{"class":83,"line":317},[284,2325,2326],{"class":414},"Revision ID: 3f7a1c8b2e49\n",[284,2328,2329],{"class":83,"line":335},[284,2330,2331],{"class":414},"Revises: 9d2e5a4b1c73\n",[284,2333,2334],{"class":83,"line":390},[284,2335,2336],{"class":414},"Create Date: 2026-06-18 09:14:32.887123\n",[284,2338,2339],{"class":83,"line":405},[284,2340,2341],{"class":414},"\"\"\"\n",[284,2343,2344,2346,2349,2352],{"class":83,"line":418},[284,2345,297],{"class":296},[284,2347,2348],{"class":426}," __future__",[284,2350,2351],{"class":296}," import",[284,2353,2354],{"class":300}," annotations\n",[284,2356,2357],{"class":83,"line":423},[284,2358,314],{"emptyLinePlaceholder":313},[284,2360,2361,2363,2365,2367],{"class":83,"line":456},[284,2362,297],{"class":296},[284,2364,791],{"class":300},[284,2366,304],{"class":296},[284,2368,796],{"class":300},[284,2370,2371,2373,2375,2377],{"class":83,"line":498},[284,2372,304],{"class":296},[284,2374,355],{"class":300},[284,2376,805],{"class":296},[284,2378,808],{"class":300},[284,2380,2381],{"class":83,"line":509},[284,2382,314],{"emptyLinePlaceholder":313},[284,2384,2385,2388,2390,2393],{"class":83,"line":532},[284,2386,2387],{"class":300},"revision: ",[284,2389,462],{"class":426},[284,2391,2392],{"class":296}," =",[284,2394,2395],{"class":414}," \"3f7a1c8b2e49\"\n",[284,2397,2398,2401,2403,2406,2409,2411],{"class":83,"line":902},[284,2399,2400],{"class":300},"down_revision: ",[284,2402,462],{"class":426},[284,2404,2405],{"class":296}," |",[284,2407,2408],{"class":426}," tuple",[284,2410,2392],{"class":296},[284,2412,2413],{"class":414}," \"9d2e5a4b1c73\"\n",[284,2415,2416,2419,2421,2423,2425,2427,2430,2432],{"class":83,"line":907},[284,2417,2418],{"class":300},"branch_labels: ",[284,2420,462],{"class":426},[284,2422,2405],{"class":296},[284,2424,2408],{"class":426},[284,2426,2405],{"class":296},[284,2428,2429],{"class":426}," None",[284,2431,2392],{"class":296},[284,2433,2434],{"class":426}," None\n",[284,2436,2437,2440,2442,2444,2446,2448,2450,2452],{"class":83,"line":912},[284,2438,2439],{"class":300},"depends_on: ",[284,2441,462],{"class":426},[284,2443,2405],{"class":296},[284,2445,2408],{"class":426},[284,2447,2405],{"class":296},[284,2449,2429],{"class":426},[284,2451,2392],{"class":296},[284,2453,2434],{"class":426},[284,2455,2456],{"class":83,"line":918},[284,2457,314],{"emptyLinePlaceholder":313},[284,2459,2460],{"class":83,"line":929},[284,2461,314],{"emptyLinePlaceholder":313},[284,2463,2464,2466,2468,2470,2472],{"class":83,"line":940},[284,2465,817],{"class":296},[284,2467,683],{"class":323},[284,2469,822],{"class":300},[284,2471,587],{"class":426},[284,2473,827],{"class":300},[284,2475,2476],{"class":83,"line":951},[284,2477,2478],{"class":300},"    op.add_column(\n",[284,2480,2481,2484],{"class":83,"line":962},[284,2482,2483],{"class":414},"        \"invoices\"",[284,2485,869],{"class":300},[284,2487,2488],{"class":83,"line":968},[284,2489,2490],{"class":300},"        sa.Column(\n",[284,2492,2493,2496],{"class":83,"line":979},[284,2494,2495],{"class":414},"            \"due_date\"",[284,2497,869],{"class":300},[284,2499,2500],{"class":83,"line":985},[284,2501,2502],{"class":300},"            sa.Date(),\n",[284,2504,2505,2508,2510,2512,2515],{"class":83,"line":991},[284,2506,2507],{"class":444},"            nullable",[284,2509,411],{"class":296},[284,2511,450],{"class":426},[284,2513,2514],{"class":300},",  ",[284,2516,2517],{"class":289},"# always nullable first — see zero-downtime section\n",[284,2519,2520],{"class":83,"line":997},[284,2521,2522],{"class":300},"        ),\n",[284,2524,2525],{"class":83,"line":1008},[284,2526,535],{"class":300},[284,2528,2529],{"class":83,"line":1020},[284,2530,314],{"emptyLinePlaceholder":313},[284,2532,2533],{"class":83,"line":1026},[284,2534,314],{"emptyLinePlaceholder":313},[284,2536,2537,2539,2541,2543,2545],{"class":83,"line":1032},[284,2538,817],{"class":296},[284,2540,726],{"class":323},[284,2542,822],{"class":300},[284,2544,587],{"class":426},[284,2546,827],{"class":300},[284,2548,2549,2552,2555,2557,2560],{"class":83,"line":1038},[284,2550,2551],{"class":300},"    op.drop_column(",[284,2553,2554],{"class":414},"\"invoices\"",[284,2556,485],{"class":300},[284,2558,2559],{"class":414},"\"due_date\"",[284,2561,453],{"class":300},[255,2563,2565],{"id":2564},"what-autogenerate-can-and-cannot-detect","What Autogenerate Can and Cannot Detect",[14,2567,2568,2569,2572,2573,2576],{},"Alembic autogenerate handles: column additions\u002Fremovals, column type changes (with ",[24,2570,2571],{},"compare_type=True","), index creation\u002Fdeletion, foreign key changes, ",[24,2574,2575],{},"UniqueConstraint"," changes, and table creation\u002Fdeletion.",[14,2578,2579,2580,2583,2584,2587],{},"It does ",[263,2581,2582],{},"not"," automatically detect: stored procedures, triggers, views, partial indexes (without custom comparison functions), CHECK constraints (dialect-dependent), column reordering, or server-default changes on some dialects unless ",[24,2585,2586],{},"compare_server_default=True"," is set and the dialect supports it.",[14,2589,2590,2591,2593,2594,2597,2598,631],{},"Always audit the generated script before applying it to production. Pay particular attention to ",[24,2592,2017],{}," and ",[24,2595,2596],{},"DROP COLUMN"," statements — autogenerate may emit these when a model import is missing from ",[24,2599,30],{},[250,2601,2603],{"id":2602},"key-component-deep-dive-3-zero-downtime-schema-change-strategies","Key Component Deep-Dive 3: Zero-Downtime Schema Change Strategies",[255,2605,2607],{"id":2606},"the-expandcontract-pattern","The Expand\u002FContract Pattern",[14,2609,2610],{},"The fundamental principle of zero-downtime migrations is that schema changes must be backward-compatible with the currently running application version during the deployment window when both old and new code may be serving requests simultaneously. The expand\u002Fcontract pattern splits each breaking change into three phases:",[2612,2613,2614,2620,2626],"ol",{},[572,2615,2616,2619],{},[263,2617,2618],{},"Expand",": Add the new structure (new column, new table, new index) without removing anything. Old code ignores the new column; new code writes to it.",[572,2621,2622,2625],{},[263,2623,2624],{},"Migrate data",": Backfill the new column\u002Ftable with existing data. Do this in batches to avoid locking.",[572,2627,2628,2631],{},[263,2629,2630],{},"Contract",": Remove the old structure once all application instances have been updated and the backfill is complete.",[275,2633,2635],{"className":277,"code":2634,"language":279,"meta":280,"style":280},"# Phase 1 — Expand: add nullable column (no lock on Postgres)\ndef upgrade() -> None:\n    op.add_column(\n        \"orders\",\n        sa.Column(\"fulfilled_at\", sa.DateTime(timezone=True), nullable=True),\n    )\n\ndef downgrade() -> None:\n    op.drop_column(\"orders\", \"fulfilled_at\")\n",[24,2636,2637,2642,2654,2658,2664,2691,2695,2699,2711],{"__ignoreMap":280},[284,2638,2639],{"class":83,"line":286},[284,2640,2641],{"class":289},"# Phase 1 — Expand: add nullable column (no lock on Postgres)\n",[284,2643,2644,2646,2648,2650,2652],{"class":83,"line":293},[284,2645,817],{"class":296},[284,2647,683],{"class":323},[284,2649,822],{"class":300},[284,2651,587],{"class":426},[284,2653,827],{"class":300},[284,2655,2656],{"class":83,"line":310},[284,2657,2478],{"class":300},[284,2659,2660,2662],{"class":83,"line":317},[284,2661,866],{"class":414},[284,2663,869],{"class":300},[284,2665,2666,2669,2672,2675,2677,2679,2681,2683,2685,2687,2689],{"class":83,"line":335},[284,2667,2668],{"class":300},"        sa.Column(",[284,2670,2671],{"class":414},"\"fulfilled_at\"",[284,2673,2674],{"class":300},", sa.DateTime(",[284,2676,515],{"class":444},[284,2678,411],{"class":296},[284,2680,450],{"class":426},[284,2682,475],{"class":300},[284,2684,488],{"class":444},[284,2686,411],{"class":296},[284,2688,450],{"class":426},[284,2690,1087],{"class":300},[284,2692,2693],{"class":83,"line":390},[284,2694,535],{"class":300},[284,2696,2697],{"class":83,"line":405},[284,2698,314],{"emptyLinePlaceholder":313},[284,2700,2701,2703,2705,2707,2709],{"class":83,"line":418},[284,2702,817],{"class":296},[284,2704,726],{"class":323},[284,2706,822],{"class":300},[284,2708,587],{"class":426},[284,2710,827],{"class":300},[284,2712,2713,2715,2718,2720,2722],{"class":83,"line":423},[284,2714,2551],{"class":300},[284,2716,2717],{"class":414},"\"orders\"",[284,2719,485],{"class":300},[284,2721,2671],{"class":414},[284,2723,453],{"class":300},[275,2725,2727],{"className":277,"code":2726,"language":279,"meta":280,"style":280},"# Phase 2 — Backfill (separate revision, run after app deploy)\ndef upgrade() -> None:\n    # Batch update in chunks to avoid row-level lock accumulation\n    op.execute(\"\"\"\n        UPDATE orders\n        SET fulfilled_at = completed_at\n        WHERE fulfilled_at IS NULL\n          AND status = 'fulfilled'\n    \"\"\")\n\ndef downgrade() -> None:\n    pass  # Backfill reversal not required — just nullify\n",[24,2728,2729,2734,2746,2751,2758,2763,2768,2773,2778,2785,2789,2801],{"__ignoreMap":280},[284,2730,2731],{"class":83,"line":286},[284,2732,2733],{"class":289},"# Phase 2 — Backfill (separate revision, run after app deploy)\n",[284,2735,2736,2738,2740,2742,2744],{"class":83,"line":293},[284,2737,817],{"class":296},[284,2739,683],{"class":323},[284,2741,822],{"class":300},[284,2743,587],{"class":426},[284,2745,827],{"class":300},[284,2747,2748],{"class":83,"line":310},[284,2749,2750],{"class":289},"    # Batch update in chunks to avoid row-level lock accumulation\n",[284,2752,2753,2756],{"class":83,"line":317},[284,2754,2755],{"class":300},"    op.execute(",[284,2757,2341],{"class":414},[284,2759,2760],{"class":83,"line":335},[284,2761,2762],{"class":414},"        UPDATE orders\n",[284,2764,2765],{"class":83,"line":390},[284,2766,2767],{"class":414},"        SET fulfilled_at = completed_at\n",[284,2769,2770],{"class":83,"line":405},[284,2771,2772],{"class":414},"        WHERE fulfilled_at IS NULL\n",[284,2774,2775],{"class":83,"line":418},[284,2776,2777],{"class":414},"          AND status = 'fulfilled'\n",[284,2779,2780,2783],{"class":83,"line":423},[284,2781,2782],{"class":414},"    \"\"\"",[284,2784,453],{"class":300},[284,2786,2787],{"class":83,"line":456},[284,2788,314],{"emptyLinePlaceholder":313},[284,2790,2791,2793,2795,2797,2799],{"class":83,"line":498},[284,2792,817],{"class":296},[284,2794,726],{"class":323},[284,2796,822],{"class":300},[284,2798,587],{"class":426},[284,2800,827],{"class":300},[284,2802,2803,2806],{"class":83,"line":509},[284,2804,2805],{"class":296},"    pass",[284,2807,2808],{"class":289},"  # Backfill reversal not required — just nullify\n",[275,2810,2812],{"className":277,"code":2811,"language":279,"meta":280,"style":280},"# Phase 3 — Contract: add NOT NULL constraint (Postgres 12+ validates without full lock)\ndef upgrade() -> None:\n    # First add as NOT VALID to avoid full-table lock during validation\n    op.execute(\"\"\"\n        ALTER TABLE orders\n        ADD CONSTRAINT orders_fulfilled_at_nn\n        CHECK (fulfilled_at IS NOT NULL) NOT VALID\n    \"\"\")\n    # Validate separately — uses ShareUpdateExclusiveLock, not AccessExclusiveLock\n    op.execute(\"ALTER TABLE orders VALIDATE CONSTRAINT orders_fulfilled_at_nn\")\n\ndef downgrade() -> None:\n    op.execute(\"ALTER TABLE orders DROP CONSTRAINT orders_fulfilled_at_nn\")\n",[24,2813,2814,2819,2831,2836,2842,2847,2852,2857,2863,2868,2877,2881,2893],{"__ignoreMap":280},[284,2815,2816],{"class":83,"line":286},[284,2817,2818],{"class":289},"# Phase 3 — Contract: add NOT NULL constraint (Postgres 12+ validates without full lock)\n",[284,2820,2821,2823,2825,2827,2829],{"class":83,"line":293},[284,2822,817],{"class":296},[284,2824,683],{"class":323},[284,2826,822],{"class":300},[284,2828,587],{"class":426},[284,2830,827],{"class":300},[284,2832,2833],{"class":83,"line":310},[284,2834,2835],{"class":289},"    # First add as NOT VALID to avoid full-table lock during validation\n",[284,2837,2838,2840],{"class":83,"line":317},[284,2839,2755],{"class":300},[284,2841,2341],{"class":414},[284,2843,2844],{"class":83,"line":335},[284,2845,2846],{"class":414},"        ALTER TABLE orders\n",[284,2848,2849],{"class":83,"line":390},[284,2850,2851],{"class":414},"        ADD CONSTRAINT orders_fulfilled_at_nn\n",[284,2853,2854],{"class":83,"line":405},[284,2855,2856],{"class":414},"        CHECK (fulfilled_at IS NOT NULL) NOT VALID\n",[284,2858,2859,2861],{"class":83,"line":418},[284,2860,2782],{"class":414},[284,2862,453],{"class":300},[284,2864,2865],{"class":83,"line":423},[284,2866,2867],{"class":289},"    # Validate separately — uses ShareUpdateExclusiveLock, not AccessExclusiveLock\n",[284,2869,2870,2872,2875],{"class":83,"line":456},[284,2871,2755],{"class":300},[284,2873,2874],{"class":414},"\"ALTER TABLE orders VALIDATE CONSTRAINT orders_fulfilled_at_nn\"",[284,2876,453],{"class":300},[284,2878,2879],{"class":83,"line":498},[284,2880,314],{"emptyLinePlaceholder":313},[284,2882,2883,2885,2887,2889,2891],{"class":83,"line":509},[284,2884,817],{"class":296},[284,2886,726],{"class":323},[284,2888,822],{"class":300},[284,2890,587],{"class":426},[284,2892,827],{"class":300},[284,2894,2895,2897,2900],{"class":83,"line":532},[284,2896,2755],{"class":300},[284,2898,2899],{"class":414},"\"ALTER TABLE orders DROP CONSTRAINT orders_fulfilled_at_nn\"",[284,2901,453],{"class":300},[255,2903,2905],{"id":2904},"adding-a-not-null-column-without-locking","Adding a NOT NULL Column Without Locking",[14,2907,2908,2909,2912,2913,2916,2917,2920],{},"On PostgreSQL, ",[24,2910,2911],{},"ALTER TABLE ADD COLUMN col TYPE NOT NULL DEFAULT val"," rewrites the entire table in Postgres versions prior to 11. From Postgres 11 onward, adding a column with a constant ",[24,2914,2915],{},"DEFAULT"," is instantaneous because the default is stored in the catalog rather than rewritten into every row. Even so, the safe cross-version recipe is the autogenerate-friendly approach that avoids ",[24,2918,2919],{},"zero-downtime-schema-migration-strategies"," surprises:",[275,2922,2924],{"className":277,"code":2923,"language":279,"meta":280,"style":280},"# Step 1: Add nullable, no default\ndef upgrade_step1() -> None:\n    op.add_column(\"products\", sa.Column(\"sku\", sa.String(64), nullable=True))\n\n\n# Step 2: Backfill (application version N+1 writes sku on new rows)\ndef upgrade_step2() -> None:\n    op.execute(\"UPDATE products SET sku = 'LEGACY-' || id::text WHERE sku IS NULL\")\n\n\n# Step 3: Set NOT NULL (fast on Postgres 12+ using NOT VALID + VALIDATE pattern)\ndef upgrade_step3() -> None:\n    op.alter_column(\"products\", \"sku\", nullable=False)\n",[24,2925,2926,2931,2944,2973,2977,2981,2986,2999,3008,3012,3016,3021,3034],{"__ignoreMap":280},[284,2927,2928],{"class":83,"line":286},[284,2929,2930],{"class":289},"# Step 1: Add nullable, no default\n",[284,2932,2933,2935,2938,2940,2942],{"class":83,"line":293},[284,2934,817],{"class":296},[284,2936,2937],{"class":323}," upgrade_step1",[284,2939,822],{"class":300},[284,2941,587],{"class":426},[284,2943,827],{"class":300},[284,2945,2946,2949,2952,2955,2958,2961,2963,2965,2967,2969,2971],{"class":83,"line":310},[284,2947,2948],{"class":300},"    op.add_column(",[284,2950,2951],{"class":414},"\"products\"",[284,2953,2954],{"class":300},", sa.Column(",[284,2956,2957],{"class":414},"\"sku\"",[284,2959,2960],{"class":300},", sa.String(",[284,2962,66],{"class":426},[284,2964,475],{"class":300},[284,2966,488],{"class":444},[284,2968,411],{"class":296},[284,2970,450],{"class":426},[284,2972,976],{"class":300},[284,2974,2975],{"class":83,"line":317},[284,2976,314],{"emptyLinePlaceholder":313},[284,2978,2979],{"class":83,"line":335},[284,2980,314],{"emptyLinePlaceholder":313},[284,2982,2983],{"class":83,"line":390},[284,2984,2985],{"class":289},"# Step 2: Backfill (application version N+1 writes sku on new rows)\n",[284,2987,2988,2990,2993,2995,2997],{"class":83,"line":405},[284,2989,817],{"class":296},[284,2991,2992],{"class":323}," upgrade_step2",[284,2994,822],{"class":300},[284,2996,587],{"class":426},[284,2998,827],{"class":300},[284,3000,3001,3003,3006],{"class":83,"line":418},[284,3002,2755],{"class":300},[284,3004,3005],{"class":414},"\"UPDATE products SET sku = 'LEGACY-' || id::text WHERE sku IS NULL\"",[284,3007,453],{"class":300},[284,3009,3010],{"class":83,"line":423},[284,3011,314],{"emptyLinePlaceholder":313},[284,3013,3014],{"class":83,"line":456},[284,3015,314],{"emptyLinePlaceholder":313},[284,3017,3018],{"class":83,"line":498},[284,3019,3020],{"class":289},"# Step 3: Set NOT NULL (fast on Postgres 12+ using NOT VALID + VALIDATE pattern)\n",[284,3022,3023,3025,3028,3030,3032],{"class":83,"line":509},[284,3024,817],{"class":296},[284,3026,3027],{"class":323}," upgrade_step3",[284,3029,822],{"class":300},[284,3031,587],{"class":426},[284,3033,827],{"class":300},[284,3035,3036,3039,3041,3043,3045,3047,3049,3051,3053],{"class":83,"line":532},[284,3037,3038],{"class":300},"    op.alter_column(",[284,3040,2951],{"class":414},[284,3042,485],{"class":300},[284,3044,2957],{"class":414},[284,3046,485],{"class":300},[284,3048,488],{"class":444},[284,3050,411],{"class":296},[284,3052,493],{"class":426},[284,3054,453],{"class":300},[14,3056,3057,3058,3062],{},"For the complete walkthrough of this pattern, see the ",[18,3059,3061],{"href":3060},"\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002F","zero-downtime schema migration strategies"," guide.",[255,3064,3066],{"id":3065},"concurrent-index-creation","Concurrent Index Creation",[14,3068,3069,3070,3073,3074,3077,3078,3081],{},"Creating an index on a large table with the standard ",[24,3071,3072],{},"CREATE INDEX"," acquires a lock that blocks writes for the duration of the build. PostgreSQL offers ",[24,3075,3076],{},"CREATE INDEX CONCURRENTLY"," to build the index without a write lock, but Alembic's ",[24,3079,3080],{},"op.create_index()"," uses the standard form by default. To use the concurrent form:",[275,3083,3085],{"className":277,"code":3084,"language":279,"meta":280,"style":280},"from alembic import op\nimport sqlalchemy as sa\n\ndef upgrade() -> None:\n    # postgresql_concurrently=True uses CREATE INDEX CONCURRENTLY\n    # Must NOT be wrapped in a transaction — disable autocommit\n    op.execute(\"COMMIT\")  # End the Alembic-managed transaction\n    op.create_index(\n        \"ix_orders_customer_id\",\n        \"orders\",\n        [\"customer_id\"],\n        postgresql_concurrently=True,\n    )\n\ndef downgrade() -> None:\n    op.execute(\"COMMIT\")\n    op.drop_index(\"ix_orders_customer_id\", table_name=\"orders\", postgresql_concurrently=True)\n",[24,3086,3087,3097,3107,3111,3123,3128,3133,3146,3151,3158,3164,3175,3186,3190,3194,3206,3214],{"__ignoreMap":280},[284,3088,3089,3091,3093,3095],{"class":83,"line":286},[284,3090,297],{"class":296},[284,3092,791],{"class":300},[284,3094,304],{"class":296},[284,3096,796],{"class":300},[284,3098,3099,3101,3103,3105],{"class":83,"line":293},[284,3100,304],{"class":296},[284,3102,355],{"class":300},[284,3104,805],{"class":296},[284,3106,808],{"class":300},[284,3108,3109],{"class":83,"line":310},[284,3110,314],{"emptyLinePlaceholder":313},[284,3112,3113,3115,3117,3119,3121],{"class":83,"line":317},[284,3114,817],{"class":296},[284,3116,683],{"class":323},[284,3118,822],{"class":300},[284,3120,587],{"class":426},[284,3122,827],{"class":300},[284,3124,3125],{"class":83,"line":335},[284,3126,3127],{"class":289},"    # postgresql_concurrently=True uses CREATE INDEX CONCURRENTLY\n",[284,3129,3130],{"class":83,"line":390},[284,3131,3132],{"class":289},"    # Must NOT be wrapped in a transaction — disable autocommit\n",[284,3134,3135,3137,3140,3143],{"class":83,"line":405},[284,3136,2755],{"class":300},[284,3138,3139],{"class":414},"\"COMMIT\"",[284,3141,3142],{"class":300},")  ",[284,3144,3145],{"class":289},"# End the Alembic-managed transaction\n",[284,3147,3148],{"class":83,"line":418},[284,3149,3150],{"class":300},"    op.create_index(\n",[284,3152,3153,3156],{"class":83,"line":423},[284,3154,3155],{"class":414},"        \"ix_orders_customer_id\"",[284,3157,869],{"class":300},[284,3159,3160,3162],{"class":83,"line":456},[284,3161,866],{"class":414},[284,3163,869],{"class":300},[284,3165,3166,3169,3172],{"class":83,"line":498},[284,3167,3168],{"class":300},"        [",[284,3170,3171],{"class":414},"\"customer_id\"",[284,3173,3174],{"class":300},"],\n",[284,3176,3177,3180,3182,3184],{"class":83,"line":509},[284,3178,3179],{"class":444},"        postgresql_concurrently",[284,3181,411],{"class":296},[284,3183,450],{"class":426},[284,3185,869],{"class":300},[284,3187,3188],{"class":83,"line":532},[284,3189,535],{"class":300},[284,3191,3192],{"class":83,"line":902},[284,3193,314],{"emptyLinePlaceholder":313},[284,3195,3196,3198,3200,3202,3204],{"class":83,"line":907},[284,3197,817],{"class":296},[284,3199,726],{"class":323},[284,3201,822],{"class":300},[284,3203,587],{"class":426},[284,3205,827],{"class":300},[284,3207,3208,3210,3212],{"class":83,"line":912},[284,3209,2755],{"class":300},[284,3211,3139],{"class":414},[284,3213,453],{"class":300},[284,3215,3216,3219,3222,3224,3227,3229,3231,3233,3236,3238,3240],{"class":83,"line":918},[284,3217,3218],{"class":300},"    op.drop_index(",[284,3220,3221],{"class":414},"\"ix_orders_customer_id\"",[284,3223,485],{"class":300},[284,3225,3226],{"class":444},"table_name",[284,3228,411],{"class":296},[284,3230,2717],{"class":414},[284,3232,485],{"class":300},[284,3234,3235],{"class":444},"postgresql_concurrently",[284,3237,411],{"class":296},[284,3239,450],{"class":426},[284,3241,453],{"class":300},[14,3243,3244,3245,3247,3248,3251],{},"In the async ",[24,3246,30],{},", set ",[24,3249,3250],{},"isolation_level=\"AUTOCOMMIT\""," on the connection before running concurrent index migrations:",[275,3253,3255],{"className":277,"code":3254,"language":279,"meta":280,"style":280},"async def run_concurrent_index_migration() -> None:\n    engine = create_async_engine(DATABASE_URL, poolclass=pool.NullPool)\n    async with engine.connect() as conn:\n        await conn.execution_options(isolation_level=\"AUTOCOMMIT\")\n        await conn.run_sync(do_run_migrations)\n    await engine.dispose()\n",[24,3256,3257,3272,3295,3309,3326,3333],{"__ignoreMap":280},[284,3258,3259,3261,3263,3266,3268,3270],{"class":83,"line":286},[284,3260,1575],{"class":296},[284,3262,1578],{"class":296},[284,3264,3265],{"class":323}," run_concurrent_index_migration",[284,3267,822],{"class":300},[284,3269,587],{"class":426},[284,3271,827],{"class":300},[284,3273,3274,3277,3279,3282,3285,3287,3290,3292],{"class":83,"line":293},[284,3275,3276],{"class":300},"    engine ",[284,3278,411],{"class":296},[284,3280,3281],{"class":300}," create_async_engine(",[284,3283,3284],{"class":426},"DATABASE_URL",[284,3286,485],{"class":300},[284,3288,3289],{"class":444},"poolclass",[284,3291,411],{"class":296},[284,3293,3294],{"class":300},"pool.NullPool)\n",[284,3296,3297,3299,3301,3304,3306],{"class":83,"line":310},[284,3298,1644],{"class":296},[284,3300,1647],{"class":296},[284,3302,3303],{"class":300}," engine.connect() ",[284,3305,805],{"class":296},[284,3307,3308],{"class":300}," conn:\n",[284,3310,3311,3313,3316,3319,3321,3324],{"class":83,"line":317},[284,3312,1661],{"class":296},[284,3314,3315],{"class":300}," conn.execution_options(",[284,3317,3318],{"class":444},"isolation_level",[284,3320,411],{"class":296},[284,3322,3323],{"class":414},"\"AUTOCOMMIT\"",[284,3325,453],{"class":300},[284,3327,3328,3330],{"class":83,"line":335},[284,3329,1661],{"class":296},[284,3331,3332],{"class":300}," conn.run_sync(do_run_migrations)\n",[284,3334,3335,3337],{"class":83,"line":390},[284,3336,1675],{"class":296},[284,3338,3339],{"class":300}," engine.dispose()\n",[255,3341,3343],{"id":3342},"batch-operations-for-sqlite","Batch Operations for SQLite",[14,3345,3346,3347,3350,3351,3354,3355,3358,3359,3362],{},"SQLite does not support ",[24,3348,3349],{},"ALTER TABLE ... DROP COLUMN"," or ",[24,3352,3353],{},"ALTER TABLE ... RENAME COLUMN"," in older versions, and has no ",[24,3356,3357],{},"ALTER TABLE ... ALTER COLUMN"," at all. Alembic's ",[263,3360,3361],{},"batch mode"," works around this by recreating the table under a temporary name, copying all data, then renaming:",[275,3364,3366],{"className":277,"code":3365,"language":279,"meta":280,"style":280},"from alembic import op\nimport sqlalchemy as sa\n\ndef upgrade() -> None:\n    with op.batch_alter_table(\"invoices\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"currency\", sa.String(3), nullable=False, server_default=\"USD\"))\n        batch_op.drop_column(\"currency_code\")  # old name\n        batch_op.alter_column(\"amount\", type_=sa.Numeric(12, 2))\n\ndef downgrade() -> None:\n    with op.batch_alter_table(\"invoices\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"currency_code\", sa.String(3), nullable=False, server_default=\"USD\"))\n        batch_op.drop_column(\"currency\")\n        batch_op.alter_column(\"amount\", type_=sa.Numeric(10, 2))\n",[24,3367,3368,3378,3388,3392,3404,3430,3461,3474,3502,3506,3518,3540,3568,3576],{"__ignoreMap":280},[284,3369,3370,3372,3374,3376],{"class":83,"line":286},[284,3371,297],{"class":296},[284,3373,791],{"class":300},[284,3375,304],{"class":296},[284,3377,796],{"class":300},[284,3379,3380,3382,3384,3386],{"class":83,"line":293},[284,3381,304],{"class":296},[284,3383,355],{"class":300},[284,3385,805],{"class":296},[284,3387,808],{"class":300},[284,3389,3390],{"class":83,"line":310},[284,3391,314],{"emptyLinePlaceholder":313},[284,3393,3394,3396,3398,3400,3402],{"class":83,"line":317},[284,3395,817],{"class":296},[284,3397,683],{"class":323},[284,3399,822],{"class":300},[284,3401,587],{"class":426},[284,3403,827],{"class":300},[284,3405,3406,3408,3411,3413,3415,3418,3420,3422,3425,3427],{"class":83,"line":335},[284,3407,1467],{"class":296},[284,3409,3410],{"class":300}," op.batch_alter_table(",[284,3412,2554],{"class":414},[284,3414,485],{"class":300},[284,3416,3417],{"class":444},"schema",[284,3419,411],{"class":296},[284,3421,587],{"class":426},[284,3423,3424],{"class":300},") ",[284,3426,805],{"class":296},[284,3428,3429],{"class":300}," batch_op:\n",[284,3431,3432,3435,3438,3440,3442,3444,3446,3448,3450,3452,3454,3456,3459],{"class":83,"line":390},[284,3433,3434],{"class":300},"        batch_op.add_column(sa.Column(",[284,3436,3437],{"class":414},"\"currency\"",[284,3439,2960],{"class":300},[284,3441,242],{"class":426},[284,3443,475],{"class":300},[284,3445,488],{"class":444},[284,3447,411],{"class":296},[284,3449,493],{"class":426},[284,3451,485],{"class":300},[284,3453,524],{"class":444},[284,3455,411],{"class":296},[284,3457,3458],{"class":414},"\"USD\"",[284,3460,976],{"class":300},[284,3462,3463,3466,3469,3471],{"class":83,"line":405},[284,3464,3465],{"class":300},"        batch_op.drop_column(",[284,3467,3468],{"class":414},"\"currency_code\"",[284,3470,3142],{"class":300},[284,3472,3473],{"class":289},"# old name\n",[284,3475,3476,3479,3482,3484,3487,3489,3492,3495,3497,3500],{"class":83,"line":418},[284,3477,3478],{"class":300},"        batch_op.alter_column(",[284,3480,3481],{"class":414},"\"amount\"",[284,3483,485],{"class":300},[284,3485,3486],{"class":444},"type_",[284,3488,411],{"class":296},[284,3490,3491],{"class":300},"sa.Numeric(",[284,3493,3494],{"class":426},"12",[284,3496,485],{"class":300},[284,3498,3499],{"class":426},"2",[284,3501,976],{"class":300},[284,3503,3504],{"class":83,"line":423},[284,3505,314],{"emptyLinePlaceholder":313},[284,3507,3508,3510,3512,3514,3516],{"class":83,"line":456},[284,3509,817],{"class":296},[284,3511,726],{"class":323},[284,3513,822],{"class":300},[284,3515,587],{"class":426},[284,3517,827],{"class":300},[284,3519,3520,3522,3524,3526,3528,3530,3532,3534,3536,3538],{"class":83,"line":498},[284,3521,1467],{"class":296},[284,3523,3410],{"class":300},[284,3525,2554],{"class":414},[284,3527,485],{"class":300},[284,3529,3417],{"class":444},[284,3531,411],{"class":296},[284,3533,587],{"class":426},[284,3535,3424],{"class":300},[284,3537,805],{"class":296},[284,3539,3429],{"class":300},[284,3541,3542,3544,3546,3548,3550,3552,3554,3556,3558,3560,3562,3564,3566],{"class":83,"line":509},[284,3543,3434],{"class":300},[284,3545,3468],{"class":414},[284,3547,2960],{"class":300},[284,3549,242],{"class":426},[284,3551,475],{"class":300},[284,3553,488],{"class":444},[284,3555,411],{"class":296},[284,3557,493],{"class":426},[284,3559,485],{"class":300},[284,3561,524],{"class":444},[284,3563,411],{"class":296},[284,3565,3458],{"class":414},[284,3567,976],{"class":300},[284,3569,3570,3572,3574],{"class":83,"line":532},[284,3571,3465],{"class":300},[284,3573,3437],{"class":414},[284,3575,453],{"class":300},[284,3577,3578,3580,3582,3584,3586,3588,3590,3593,3595,3597],{"class":83,"line":902},[284,3579,3478],{"class":300},[284,3581,3481],{"class":414},[284,3583,485],{"class":300},[284,3585,3486],{"class":444},[284,3587,411],{"class":296},[284,3589,3491],{"class":300},[284,3591,3592],{"class":426},"10",[284,3594,485],{"class":300},[284,3596,3499],{"class":426},[284,3598,976],{"class":300},[14,3600,3601,3602,3606,3607,3610,3611,3614],{},"Batch mode works on any dialect but is most commonly needed for SQLite-backed test databases. The ",[18,3603,3605],{"href":3604},"\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002F","autogenerating and reviewing migration scripts"," guide covers how to configure ",[24,3608,3609],{},"render_as_batch=True"," in ",[24,3612,3613],{},"alembic.ini"," for projects that need batch mode by default.",[250,3616,3618],{"id":3617},"advanced-patterns-production-configuration","Advanced Patterns & Production Configuration",[255,3620,3622],{"id":3621},"running-migrations-in-cicd","Running Migrations in CI\u002FCD",[14,3624,3625,3626,3628],{},"The most reliable CI\u002FCD migration pattern is to run ",[24,3627,610],{}," as a pre-deployment step, before your new application containers start. Container orchestration platforms (Kubernetes, ECS) support this as an init container or a migration job.",[275,3630,3634],{"className":3631,"code":3632,"language":3633,"meta":280,"style":280},"language-dockerfile shiki shiki-themes github-light github-dark","# Dockerfile — migration stage\nFROM python:3.11-slim AS migrator\nWORKDIR \u002Fapp\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nCMD [\"alembic\", \"upgrade\", \"head\"]\n","dockerfile",[24,3635,3636,3641,3646,3651,3656,3661,3666],{"__ignoreMap":280},[284,3637,3638],{"class":83,"line":286},[284,3639,3640],{},"# Dockerfile — migration stage\n",[284,3642,3643],{"class":83,"line":293},[284,3644,3645],{},"FROM python:3.11-slim AS migrator\n",[284,3647,3648],{"class":83,"line":310},[284,3649,3650],{},"WORKDIR \u002Fapp\n",[284,3652,3653],{"class":83,"line":317},[284,3654,3655],{},"COPY requirements.txt .\n",[284,3657,3658],{"class":83,"line":335},[284,3659,3660],{},"RUN pip install --no-cache-dir -r requirements.txt\n",[284,3662,3663],{"class":83,"line":390},[284,3664,3665],{},"COPY . .\n",[284,3667,3668],{"class":83,"line":405},[284,3669,3670],{},"CMD [\"alembic\", \"upgrade\", \"head\"]\n",[275,3672,3676],{"className":3673,"code":3674,"language":3675,"meta":280,"style":280},"language-yaml shiki shiki-themes github-light github-dark","# kubernetes\u002Fmigration-job.yaml\napiVersion: batch\u002Fv1\nkind: Job\nmetadata:\n  name: db-migration-{{ .Release.Revision }}\nspec:\n  template:\n    spec:\n      restartPolicy: Never\n      containers:\n        - name: alembic\n          image: myapp:{{ .Values.image.tag }}\n          command: [\"alembic\", \"upgrade\", \"head\"]\n          envFrom:\n            - secretRef:\n                name: db-credentials\n      initContainers: []\n","yaml",[24,3677,3678,3683,3694,3704,3711,3721,3728,3735,3742,3752,3759,3772,3782,3806,3813,3823,3833],{"__ignoreMap":280},[284,3679,3680],{"class":83,"line":286},[284,3681,3682],{"class":289},"# kubernetes\u002Fmigration-job.yaml\n",[284,3684,3685,3689,3691],{"class":83,"line":293},[284,3686,3688],{"class":3687},"s9eBZ","apiVersion",[284,3690,1430],{"class":300},[284,3692,3693],{"class":414},"batch\u002Fv1\n",[284,3695,3696,3699,3701],{"class":83,"line":310},[284,3697,3698],{"class":3687},"kind",[284,3700,1430],{"class":300},[284,3702,3703],{"class":414},"Job\n",[284,3705,3706,3709],{"class":83,"line":317},[284,3707,3708],{"class":3687},"metadata",[284,3710,827],{"class":300},[284,3712,3713,3716,3718],{"class":83,"line":335},[284,3714,3715],{"class":3687},"  name",[284,3717,1430],{"class":300},[284,3719,3720],{"class":414},"db-migration-{{ .Release.Revision }}\n",[284,3722,3723,3726],{"class":83,"line":390},[284,3724,3725],{"class":3687},"spec",[284,3727,827],{"class":300},[284,3729,3730,3733],{"class":83,"line":405},[284,3731,3732],{"class":3687},"  template",[284,3734,827],{"class":300},[284,3736,3737,3740],{"class":83,"line":418},[284,3738,3739],{"class":3687},"    spec",[284,3741,827],{"class":300},[284,3743,3744,3747,3749],{"class":83,"line":423},[284,3745,3746],{"class":3687},"      restartPolicy",[284,3748,1430],{"class":300},[284,3750,3751],{"class":414},"Never\n",[284,3753,3754,3757],{"class":83,"line":456},[284,3755,3756],{"class":3687},"      containers",[284,3758,827],{"class":300},[284,3760,3761,3764,3767,3769],{"class":83,"line":498},[284,3762,3763],{"class":300},"        - ",[284,3765,3766],{"class":3687},"name",[284,3768,1430],{"class":300},[284,3770,3771],{"class":414},"alembic\n",[284,3773,3774,3777,3779],{"class":83,"line":509},[284,3775,3776],{"class":3687},"          image",[284,3778,1430],{"class":300},[284,3780,3781],{"class":414},"myapp:{{ .Values.image.tag }}\n",[284,3783,3784,3787,3790,3793,3795,3798,3800,3803],{"class":83,"line":532},[284,3785,3786],{"class":3687},"          command",[284,3788,3789],{"class":300},": [",[284,3791,3792],{"class":414},"\"alembic\"",[284,3794,485],{"class":300},[284,3796,3797],{"class":414},"\"upgrade\"",[284,3799,485],{"class":300},[284,3801,3802],{"class":414},"\"head\"",[284,3804,3805],{"class":300},"]\n",[284,3807,3808,3811],{"class":83,"line":902},[284,3809,3810],{"class":3687},"          envFrom",[284,3812,827],{"class":300},[284,3814,3815,3818,3821],{"class":83,"line":907},[284,3816,3817],{"class":300},"            - ",[284,3819,3820],{"class":3687},"secretRef",[284,3822,827],{"class":300},[284,3824,3825,3828,3830],{"class":83,"line":912},[284,3826,3827],{"class":3687},"                name",[284,3829,1430],{"class":300},[284,3831,3832],{"class":414},"db-credentials\n",[284,3834,3835,3838],{"class":83,"line":918},[284,3836,3837],{"class":3687},"      initContainers",[284,3839,3840],{"class":300},": []\n",[14,3842,3843],{},"Fail fast if the migration job fails — never proceed to rolling out application containers with a schema that is out of sync.",[14,3845,3846,3847,3849],{},"For rollback safety, always verify that ",[24,3848,600],{}," functions are implemented and tested. A common CI check:",[275,3851,3853],{"className":666,"code":3852,"language":668,"meta":280,"style":280},"# In CI pipeline: verify round-trip integrity\nalembic upgrade head\nalembic downgrade -1\nalembic upgrade head\n",[24,3854,3855,3860,3868,3876],{"__ignoreMap":280},[284,3856,3857],{"class":83,"line":286},[284,3858,3859],{"class":289},"# In CI pipeline: verify round-trip integrity\n",[284,3861,3862,3864,3866],{"class":83,"line":293},[284,3863,680],{"class":323},[284,3865,683],{"class":414},[284,3867,1998],{"class":414},[284,3869,3870,3872,3874],{"class":83,"line":310},[284,3871,680],{"class":323},[284,3873,726],{"class":414},[284,3875,2263],{"class":426},[284,3877,3878,3880,3882],{"class":83,"line":317},[284,3879,680],{"class":323},[284,3881,683],{"class":414},[284,3883,1998],{"class":414},[255,3885,3887],{"id":3886},"multiple-bases-and-branch-labels","Multiple Bases and Branch Labels",[14,3889,3890],{},"Large applications split migrations across multiple branches — for example, separating application schema from audit logging or multi-tenant infrastructure tables. Alembic supports this via multiple bases:",[275,3892,3894],{"className":666,"code":3893,"language":668,"meta":280,"style":280},"# Create a second revision root\nalembic revision --head=base --branch-label=audit --autogenerate -m \"create_audit_log\"\n\n# Apply only the audit branch\nalembic upgrade audit@head\n\n# Apply both branches (standard and audit)\nalembic upgrade heads\n",[24,3895,3896,3901,3920,3924,3929,3938,3942,3947],{"__ignoreMap":280},[284,3897,3898],{"class":83,"line":286},[284,3899,3900],{"class":289},"# Create a second revision root\n",[284,3902,3903,3905,3907,3910,3913,3915,3917],{"class":83,"line":293},[284,3904,680],{"class":323},[284,3906,2170],{"class":414},[284,3908,3909],{"class":426}," --head=base",[284,3911,3912],{"class":426}," --branch-label=audit",[284,3914,2173],{"class":426},[284,3916,2176],{"class":426},[284,3918,3919],{"class":414}," \"create_audit_log\"\n",[284,3921,3922],{"class":83,"line":310},[284,3923,314],{"emptyLinePlaceholder":313},[284,3925,3926],{"class":83,"line":317},[284,3927,3928],{"class":289},"# Apply only the audit branch\n",[284,3930,3931,3933,3935],{"class":83,"line":335},[284,3932,680],{"class":323},[284,3934,683],{"class":414},[284,3936,3937],{"class":414}," audit@head\n",[284,3939,3940],{"class":83,"line":390},[284,3941,314],{"emptyLinePlaceholder":313},[284,3943,3944],{"class":83,"line":405},[284,3945,3946],{"class":289},"# Apply both branches (standard and audit)\n",[284,3948,3949,3951,3953],{"class":83,"line":418},[284,3950,680],{"class":323},[284,3952,683],{"class":414},[284,3954,3955],{"class":414}," heads\n",[14,3957,3958,3959,3961],{},"Each branch tracks its own head in ",[24,3960,154],{},", which allows teams to merge and deploy branch migrations independently without conflicts.",[255,3963,3965],{"id":3964},"version_table-schema-placement","version_table Schema Placement",[14,3967,3968,3969,3971,3972,3975],{},"By default, Alembic creates ",[24,3970,154],{}," in the default schema (",[24,3973,3974],{},"public"," for PostgreSQL). In multi-tenant or multi-schema applications, you may want to control this placement:",[275,3977,3979],{"className":277,"code":3978,"language":279,"meta":280,"style":280},"# In env.py context.configure()\ncontext.configure(\n    connection=connection,\n    target_metadata=target_metadata,\n    version_table=\"alembic_version\",\n    version_table_schema=\"migrations\",  # Store version table in dedicated schema\n    include_schemas=True,               # Autogenerate across all schemas\n    compare_type=True,\n    compare_server_default=True,\n)\n",[24,3980,3981,3986,3991,4000,4009,4021,4036,4051,4062,4073],{"__ignoreMap":280},[284,3982,3983],{"class":83,"line":286},[284,3984,3985],{"class":289},"# In env.py context.configure()\n",[284,3987,3988],{"class":83,"line":293},[284,3989,3990],{"class":300},"context.configure(\n",[284,3992,3993,3996,3998],{"class":83,"line":310},[284,3994,3995],{"class":444},"    connection",[284,3997,411],{"class":296},[284,3999,1516],{"class":300},[284,4001,4002,4005,4007],{"class":83,"line":317},[284,4003,4004],{"class":444},"    target_metadata",[284,4006,411],{"class":296},[284,4008,1403],{"class":300},[284,4010,4011,4014,4016,4019],{"class":83,"line":335},[284,4012,4013],{"class":444},"    version_table",[284,4015,411],{"class":296},[284,4017,4018],{"class":414},"\"alembic_version\"",[284,4020,869],{"class":300},[284,4022,4023,4026,4028,4031,4033],{"class":83,"line":390},[284,4024,4025],{"class":444},"    version_table_schema",[284,4027,411],{"class":296},[284,4029,4030],{"class":414},"\"migrations\"",[284,4032,2514],{"class":300},[284,4034,4035],{"class":289},"# Store version table in dedicated schema\n",[284,4037,4038,4041,4043,4045,4048],{"class":83,"line":405},[284,4039,4040],{"class":444},"    include_schemas",[284,4042,411],{"class":296},[284,4044,450],{"class":426},[284,4046,4047],{"class":300},",               ",[284,4049,4050],{"class":289},"# Autogenerate across all schemas\n",[284,4052,4053,4056,4058,4060],{"class":83,"line":418},[284,4054,4055],{"class":444},"    compare_type",[284,4057,411],{"class":296},[284,4059,450],{"class":426},[284,4061,869],{"class":300},[284,4063,4064,4067,4069,4071],{"class":83,"line":423},[284,4065,4066],{"class":444},"    compare_server_default",[284,4068,411],{"class":296},[284,4070,450],{"class":426},[284,4072,869],{"class":300},[284,4074,4075],{"class":83,"line":456},[284,4076,453],{"class":300},[14,4078,4079,4080,4083],{},"Pair this with ",[24,4081,4082],{},"include_object"," to filter which schemas and tables participate in autogenerate:",[275,4085,4087],{"className":277,"code":4086,"language":279,"meta":280,"style":280},"def include_object(object, name, type_, reflected, compare_to):\n    # Skip PostGIS system tables and pg_catalog artifacts\n    if type_ == \"table\" and name.startswith(\"spatial_ref_sys\"):\n        return False\n    if type_ == \"table\" and object.schema in (\"tiger\", \"topology\"):\n        return False\n    return True\n\ncontext.configure(\n    ...,\n    include_object=include_object,\n)\n",[24,4088,4089,4099,4104,4128,4136,4168,4174,4182,4186,4190,4197,4207],{"__ignoreMap":280},[284,4090,4091,4093,4096],{"class":83,"line":286},[284,4092,817],{"class":296},[284,4094,4095],{"class":323}," include_object",[284,4097,4098],{"class":300},"(object, name, type_, reflected, compare_to):\n",[284,4100,4101],{"class":83,"line":293},[284,4102,4103],{"class":289},"    # Skip PostGIS system tables and pg_catalog artifacts\n",[284,4105,4106,4109,4112,4114,4117,4120,4123,4126],{"class":83,"line":310},[284,4107,4108],{"class":296},"    if",[284,4110,4111],{"class":300}," type_ ",[284,4113,1076],{"class":296},[284,4115,4116],{"class":414}," \"table\"",[284,4118,4119],{"class":296}," and",[284,4121,4122],{"class":300}," name.startswith(",[284,4124,4125],{"class":414},"\"spatial_ref_sys\"",[284,4127,332],{"class":300},[284,4129,4130,4133],{"class":83,"line":317},[284,4131,4132],{"class":296},"        return",[284,4134,4135],{"class":426}," False\n",[284,4137,4138,4140,4142,4144,4146,4148,4151,4154,4156,4158,4161,4163,4166],{"class":83,"line":335},[284,4139,4108],{"class":296},[284,4141,4111],{"class":300},[284,4143,1076],{"class":296},[284,4145,4116],{"class":414},[284,4147,4119],{"class":296},[284,4149,4150],{"class":426}," object",[284,4152,4153],{"class":300},".schema ",[284,4155,1050],{"class":296},[284,4157,652],{"class":300},[284,4159,4160],{"class":414},"\"tiger\"",[284,4162,485],{"class":300},[284,4164,4165],{"class":414},"\"topology\"",[284,4167,332],{"class":300},[284,4169,4170,4172],{"class":83,"line":390},[284,4171,4132],{"class":296},[284,4173,4135],{"class":426},[284,4175,4176,4179],{"class":83,"line":405},[284,4177,4178],{"class":296},"    return",[284,4180,4181],{"class":426}," True\n",[284,4183,4184],{"class":83,"line":418},[284,4185,314],{"emptyLinePlaceholder":313},[284,4187,4188],{"class":83,"line":423},[284,4189,3990],{"class":300},[284,4191,4192,4195],{"class":83,"line":456},[284,4193,4194],{"class":426},"    ...",[284,4196,869],{"class":300},[284,4198,4199,4202,4204],{"class":83,"line":498},[284,4200,4201],{"class":444},"    include_object",[284,4203,411],{"class":296},[284,4205,4206],{"class":300},"include_object,\n",[284,4208,4209],{"class":83,"line":509},[284,4210,453],{"class":300},[255,4212,4214],{"id":4213},"lock-handling-and-migration-timeouts","Lock Handling and Migration Timeouts",[14,4216,4217,4218,4221],{},"On high-traffic databases, DDL migrations compete with application queries for table-level locks. Set ",[24,4219,4220],{},"lock_timeout"," at the session level to fail fast rather than queue behind long-running transactions:",[275,4223,4225],{"className":277,"code":4224,"language":279,"meta":280,"style":280},"def do_run_migrations(connection) -> None:\n    # Fail if we cannot acquire the lock within 5 seconds\n    connection.execute(sa.text(\"SET lock_timeout = '5s'\"))\n    connection.execute(sa.text(\"SET statement_timeout = '120s'\"))\n\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        compare_type=True,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n",[24,4226,4227,4239,4244,4254,4263,4267,4271,4279,4287,4297,4301,4307],{"__ignoreMap":280},[284,4228,4229,4231,4233,4235,4237],{"class":83,"line":286},[284,4230,817],{"class":296},[284,4232,1490],{"class":323},[284,4234,1493],{"class":300},[284,4236,587],{"class":426},[284,4238,827],{"class":300},[284,4240,4241],{"class":83,"line":293},[284,4242,4243],{"class":289},"    # Fail if we cannot acquire the lock within 5 seconds\n",[284,4245,4246,4249,4252],{"class":83,"line":310},[284,4247,4248],{"class":300},"    connection.execute(sa.text(",[284,4250,4251],{"class":414},"\"SET lock_timeout = '5s'\"",[284,4253,976],{"class":300},[284,4255,4256,4258,4261],{"class":83,"line":317},[284,4257,4248],{"class":300},[284,4259,4260],{"class":414},"\"SET statement_timeout = '120s'\"",[284,4262,976],{"class":300},[284,4264,4265],{"class":83,"line":335},[284,4266,314],{"emptyLinePlaceholder":313},[284,4268,4269],{"class":83,"line":390},[284,4270,1383],{"class":300},[284,4272,4273,4275,4277],{"class":83,"line":405},[284,4274,1511],{"class":444},[284,4276,411],{"class":296},[284,4278,1516],{"class":300},[284,4280,4281,4283,4285],{"class":83,"line":418},[284,4282,1398],{"class":444},[284,4284,411],{"class":296},[284,4286,1403],{"class":300},[284,4288,4289,4291,4293,4295],{"class":83,"line":423},[284,4290,1441],{"class":444},[284,4292,411],{"class":296},[284,4294,450],{"class":426},[284,4296,869],{"class":300},[284,4298,4299],{"class":83,"line":456},[284,4300,535],{"class":300},[284,4302,4303,4305],{"class":83,"line":498},[284,4304,1467],{"class":296},[284,4306,1470],{"class":300},[284,4308,4309],{"class":83,"line":509},[284,4310,1475],{"class":300},[14,4312,4313,4314,631],{},"For deployments that cannot afford even a 5-second lock wait, restructure the migration using the expand\u002Fcontract pattern so that no individual DDL statement acquires more than a ",[24,4315,4316],{},"ShareUpdateExclusiveLock",[255,4318,4320],{"id":4319},"stamping-and-baseline-management","Stamping and Baseline Management",[14,4322,4323,4324,4327],{},"When adopting Alembic for an existing database that was not previously managed by it, use ",[24,4325,4326],{},"alembic stamp"," to record a baseline without running any migrations:",[275,4329,4331],{"className":666,"code":4330,"language":668,"meta":280,"style":280},"# Record that the database is already at revision 9d2e5a4b1c73\n# without executing any upgrade() functions\nalembic stamp 9d2e5a4b1c73\n\n# Stamp to head (marks all current revisions as applied)\nalembic stamp head\n",[24,4332,4333,4338,4343,4353,4357,4362],{"__ignoreMap":280},[284,4334,4335],{"class":83,"line":286},[284,4336,4337],{"class":289},"# Record that the database is already at revision 9d2e5a4b1c73\n",[284,4339,4340],{"class":83,"line":293},[284,4341,4342],{"class":289},"# without executing any upgrade() functions\n",[284,4344,4345,4347,4350],{"class":83,"line":310},[284,4346,680],{"class":323},[284,4348,4349],{"class":414}," stamp",[284,4351,4352],{"class":414}," 9d2e5a4b1c73\n",[284,4354,4355],{"class":83,"line":317},[284,4356,314],{"emptyLinePlaceholder":313},[284,4358,4359],{"class":83,"line":335},[284,4360,4361],{"class":289},"# Stamp to head (marks all current revisions as applied)\n",[284,4363,4364,4366,4368],{"class":83,"line":390},[284,4365,680],{"class":323},[284,4367,4349],{"class":414},[284,4369,1998],{"class":414},[14,4371,4372,4373,4375,4376,4378,4379,4382],{},"This is the correct onboarding procedure for brownfield projects. Never run ",[24,4374,610],{}," against a pre-existing production database without first auditing every revision's ",[24,4377,594],{}," function — autogenerated scripts may contain ",[24,4380,4381],{},"DROP"," statements that assume a clean slate.",[255,4384,4386],{"id":4385},"pool-configuration-for-async-migration-engines","Pool Configuration for Async Migration Engines",[14,4388,4389,4390,4392,4393,4395],{},"The async engine setup for migrations differs from the application engine in the ",[18,4391,21],{"href":20}," guide. Migration engines should always use ",[24,4394,1723],{}," because:",[2612,4397,4398,4401,4404],{},[572,4399,4400],{},"Migrations run synchronously (one revision at a time).",[572,4402,4403],{},"The migration process exits after completion — pooled connections are wasted.",[572,4405,4406,4407,4409],{},"Some DDL operations (particularly ",[24,4408,3076],{},") must run outside a transaction, which is incompatible with pool-managed connection state.",[275,4411,4413],{"className":277,"code":4412,"language":279,"meta":280,"style":280},"from sqlalchemy.pool import NullPool\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nmigration_engine = create_async_engine(\n    DATABASE_URL,\n    poolclass=NullPool,\n    echo=True,   # Log all SQL during migrations for audit trail\n    future=True,\n)\n",[24,4414,4415,4427,4438,4442,4451,4458,4468,4483,4494],{"__ignoreMap":280},[284,4416,4417,4419,4422,4424],{"class":83,"line":286},[284,4418,297],{"class":296},[284,4420,4421],{"class":300}," sqlalchemy.pool ",[284,4423,304],{"class":296},[284,4425,4426],{"class":300}," NullPool\n",[284,4428,4429,4431,4433,4435],{"class":83,"line":293},[284,4430,297],{"class":296},[284,4432,1265],{"class":300},[284,4434,304],{"class":296},[284,4436,4437],{"class":300}," create_async_engine\n",[284,4439,4440],{"class":83,"line":310},[284,4441,314],{"emptyLinePlaceholder":313},[284,4443,4444,4447,4449],{"class":83,"line":317},[284,4445,4446],{"class":300},"migration_engine ",[284,4448,411],{"class":296},[284,4450,1604],{"class":300},[284,4452,4453,4456],{"class":83,"line":335},[284,4454,4455],{"class":426},"    DATABASE_URL",[284,4457,869],{"class":300},[284,4459,4460,4463,4465],{"class":83,"line":390},[284,4461,4462],{"class":444},"    poolclass",[284,4464,411],{"class":296},[284,4466,4467],{"class":300},"NullPool,\n",[284,4469,4470,4473,4475,4477,4480],{"class":83,"line":405},[284,4471,4472],{"class":444},"    echo",[284,4474,411],{"class":296},[284,4476,450],{"class":426},[284,4478,4479],{"class":300},",   ",[284,4481,4482],{"class":289},"# Log all SQL during migrations for audit trail\n",[284,4484,4485,4488,4490,4492],{"class":83,"line":418},[284,4486,4487],{"class":444},"    future",[284,4489,411],{"class":296},[284,4491,450],{"class":426},[284,4493,869],{"class":300},[284,4495,4496],{"class":83,"line":423},[284,4497,453],{"class":300},[255,4499,4501],{"id":4500},"custom-revision-templates","Custom Revision Templates",[14,4503,4504],{},"The default revision template is minimal. For production teams, a custom template enforces a house style: structured docstrings, type annotations, and explicit rollback verification comments.",[275,4506,4508],{"className":277,"code":4507,"language":279,"meta":280,"style":280},"# alembic\u002Fscript.py.mako  (Mako template, not Python — syntax is intentional)\n\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\nNotes:\n    - Reviewed by: \u003Creviewer>\n    - Rollback tested: \u003Cyes\u002Fno>\n    - Lock risk: \u003Clow\u002Fmedium\u002Fhigh>\n\"\"\"\nfrom __future__ import annotations\n\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\nrevision: str = ${repr(up_revision)}\ndown_revision: str | tuple | None = ${repr(down_revision)}\nbranch_labels: str | tuple | None = ${repr(branch_labels)}\ndepends_on: str | tuple | None = ${repr(depends_on)}\n\n\ndef upgrade() -> None:\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade() -> None:\n    ${downgrades if downgrades else \"pass\"}\n",[24,4509,4510,4515,4520,4524,4529,4534,4539,4543,4548,4553,4558,4563,4567,4577,4581,4591,4601,4623,4627,4646,4671,4696,4721,4725,4729,4741,4761,4765,4769,4781],{"__ignoreMap":280},[284,4511,4512],{"class":83,"line":286},[284,4513,4514],{"class":289},"# alembic\u002Fscript.py.mako  (Mako template, not Python — syntax is intentional)\n",[284,4516,4517],{"class":83,"line":293},[284,4518,4519],{"class":414},"\"\"\"${message}\n",[284,4521,4522],{"class":83,"line":310},[284,4523,314],{"emptyLinePlaceholder":313},[284,4525,4526],{"class":83,"line":317},[284,4527,4528],{"class":414},"Revision ID: ${up_revision}\n",[284,4530,4531],{"class":83,"line":335},[284,4532,4533],{"class":414},"Revises: ${down_revision | comma,n}\n",[284,4535,4536],{"class":83,"line":390},[284,4537,4538],{"class":414},"Create Date: ${create_date}\n",[284,4540,4541],{"class":83,"line":405},[284,4542,314],{"emptyLinePlaceholder":313},[284,4544,4545],{"class":83,"line":418},[284,4546,4547],{"class":414},"Notes:\n",[284,4549,4550],{"class":83,"line":423},[284,4551,4552],{"class":414},"    - Reviewed by: \u003Creviewer>\n",[284,4554,4555],{"class":83,"line":456},[284,4556,4557],{"class":414},"    - Rollback tested: \u003Cyes\u002Fno>\n",[284,4559,4560],{"class":83,"line":498},[284,4561,4562],{"class":414},"    - Lock risk: \u003Clow\u002Fmedium\u002Fhigh>\n",[284,4564,4565],{"class":83,"line":509},[284,4566,2341],{"class":414},[284,4568,4569,4571,4573,4575],{"class":83,"line":532},[284,4570,297],{"class":296},[284,4572,2348],{"class":426},[284,4574,2351],{"class":296},[284,4576,2354],{"class":300},[284,4578,4579],{"class":83,"line":902},[284,4580,314],{"emptyLinePlaceholder":313},[284,4582,4583,4585,4587,4589],{"class":83,"line":907},[284,4584,297],{"class":296},[284,4586,791],{"class":300},[284,4588,304],{"class":296},[284,4590,796],{"class":300},[284,4592,4593,4595,4597,4599],{"class":83,"line":912},[284,4594,304],{"class":296},[284,4596,355],{"class":300},[284,4598,805],{"class":296},[284,4600,808],{"class":300},[284,4602,4603,4607,4610,4612,4615,4617,4620],{"class":83,"line":918},[284,4604,4606],{"class":4605},"s7hpK","$",[284,4608,4609],{"class":300},"{imports ",[284,4611,1694],{"class":296},[284,4613,4614],{"class":300}," imports ",[284,4616,1709],{"class":296},[284,4618,4619],{"class":414}," \"\"",[284,4621,4622],{"class":300},"}\n",[284,4624,4625],{"class":83,"line":929},[284,4626,314],{"emptyLinePlaceholder":313},[284,4628,4629,4631,4633,4635,4638,4640,4643],{"class":83,"line":940},[284,4630,2387],{"class":300},[284,4632,462],{"class":426},[284,4634,2392],{"class":296},[284,4636,4637],{"class":4605}," $",[284,4639,1424],{"class":300},[284,4641,4642],{"class":426},"repr",[284,4644,4645],{"class":300},"(up_revision)}\n",[284,4647,4648,4650,4652,4654,4656,4658,4660,4662,4664,4666,4668],{"class":83,"line":951},[284,4649,2400],{"class":300},[284,4651,462],{"class":426},[284,4653,2405],{"class":296},[284,4655,2408],{"class":426},[284,4657,2405],{"class":296},[284,4659,2429],{"class":426},[284,4661,2392],{"class":296},[284,4663,4637],{"class":4605},[284,4665,1424],{"class":300},[284,4667,4642],{"class":426},[284,4669,4670],{"class":300},"(down_revision)}\n",[284,4672,4673,4675,4677,4679,4681,4683,4685,4687,4689,4691,4693],{"class":83,"line":962},[284,4674,2418],{"class":300},[284,4676,462],{"class":426},[284,4678,2405],{"class":296},[284,4680,2408],{"class":426},[284,4682,2405],{"class":296},[284,4684,2429],{"class":426},[284,4686,2392],{"class":296},[284,4688,4637],{"class":4605},[284,4690,1424],{"class":300},[284,4692,4642],{"class":426},[284,4694,4695],{"class":300},"(branch_labels)}\n",[284,4697,4698,4700,4702,4704,4706,4708,4710,4712,4714,4716,4718],{"class":83,"line":968},[284,4699,2439],{"class":300},[284,4701,462],{"class":426},[284,4703,2405],{"class":296},[284,4705,2408],{"class":426},[284,4707,2405],{"class":296},[284,4709,2429],{"class":426},[284,4711,2392],{"class":296},[284,4713,4637],{"class":4605},[284,4715,1424],{"class":300},[284,4717,4642],{"class":426},[284,4719,4720],{"class":300},"(depends_on)}\n",[284,4722,4723],{"class":83,"line":979},[284,4724,314],{"emptyLinePlaceholder":313},[284,4726,4727],{"class":83,"line":985},[284,4728,314],{"emptyLinePlaceholder":313},[284,4730,4731,4733,4735,4737,4739],{"class":83,"line":991},[284,4732,817],{"class":296},[284,4734,683],{"class":323},[284,4736,822],{"class":300},[284,4738,587],{"class":426},[284,4740,827],{"class":300},[284,4742,4743,4746,4749,4751,4754,4756,4759],{"class":83,"line":997},[284,4744,4745],{"class":4605},"    $",[284,4747,4748],{"class":300},"{upgrades ",[284,4750,1694],{"class":296},[284,4752,4753],{"class":300}," upgrades ",[284,4755,1709],{"class":296},[284,4757,4758],{"class":414}," \"pass\"",[284,4760,4622],{"class":300},[284,4762,4763],{"class":83,"line":1008},[284,4764,314],{"emptyLinePlaceholder":313},[284,4766,4767],{"class":83,"line":1020},[284,4768,314],{"emptyLinePlaceholder":313},[284,4770,4771,4773,4775,4777,4779],{"class":83,"line":1026},[284,4772,817],{"class":296},[284,4774,726],{"class":323},[284,4776,822],{"class":300},[284,4778,587],{"class":426},[284,4780,827],{"class":300},[284,4782,4783,4785,4788,4790,4793,4795,4797],{"class":83,"line":1032},[284,4784,4745],{"class":4605},[284,4786,4787],{"class":300},"{downgrades ",[284,4789,1694],{"class":296},[284,4791,4792],{"class":300}," downgrades ",[284,4794,1709],{"class":296},[284,4796,4758],{"class":414},[284,4798,4622],{"class":300},[14,4800,4801,4802,2112],{},"Configure the custom template location in ",[24,4803,3613],{},[275,4805,4807],{"className":1734,"code":4806,"language":1736,"meta":280,"style":280},"[alembic]\n# Path to custom revision template\nrevision_environment = true\nfile_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s_%%(slug)s\n",[24,4808,4809,4813,4818,4823],{"__ignoreMap":280},[284,4810,4811],{"class":83,"line":286},[284,4812,1748],{},[284,4814,4815],{"class":83,"line":293},[284,4816,4817],{},"# Path to custom revision template\n",[284,4819,4820],{"class":83,"line":310},[284,4821,4822],{},"revision_environment = true\n",[284,4824,4825],{"class":83,"line":317},[284,4826,4827],{},"file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s_%%(slug)s\n",[255,4829,4831],{"id":4830},"detecting-and-recovering-from-a-split-brain-version-table","Detecting and Recovering from a Split-Brain Version Table",[14,4833,4834],{},"In distributed deployments, two migration processes may run concurrently if a network partition causes a delayed migration job to fire twice. PostgreSQL advisory locks prevent this:",[275,4836,4838],{"className":277,"code":4837,"language":279,"meta":280,"style":280},"import hashlib\n\ndef do_run_migrations(connection) -> None:\n    # Acquire a session-level advisory lock keyed to the database name\n    # so only one migration process runs at a time across the cluster\n    lock_key = int(hashlib.md5(b\"alembic_migration_lock\").hexdigest()[:8], 16)\n    connection.execute(sa.text(f\"SELECT pg_advisory_lock({lock_key})\"))\n    try:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            compare_type=True,\n            compare_server_default=True,\n        )\n        with context.begin_transaction():\n            context.run_migrations()\n    finally:\n        connection.execute(sa.text(f\"SELECT pg_advisory_unlock({lock_key})\"))\n",[24,4839,4840,4847,4851,4863,4868,4873,4905,4928,4935,4940,4949,4958,4969,4980,4984,4991,4996,5003],{"__ignoreMap":280},[284,4841,4842,4844],{"class":83,"line":286},[284,4843,304],{"class":296},[284,4845,4846],{"class":300}," hashlib\n",[284,4848,4849],{"class":83,"line":293},[284,4850,314],{"emptyLinePlaceholder":313},[284,4852,4853,4855,4857,4859,4861],{"class":83,"line":310},[284,4854,817],{"class":296},[284,4856,1490],{"class":323},[284,4858,1493],{"class":300},[284,4860,587],{"class":426},[284,4862,827],{"class":300},[284,4864,4865],{"class":83,"line":317},[284,4866,4867],{"class":289},"    # Acquire a session-level advisory lock keyed to the database name\n",[284,4869,4870],{"class":83,"line":335},[284,4871,4872],{"class":289},"    # so only one migration process runs at a time across the cluster\n",[284,4874,4875,4878,4880,4883,4886,4889,4892,4895,4897,4900,4903],{"class":83,"line":390},[284,4876,4877],{"class":300},"    lock_key ",[284,4879,411],{"class":296},[284,4881,4882],{"class":426}," int",[284,4884,4885],{"class":300},"(hashlib.md5(",[284,4887,4888],{"class":296},"b",[284,4890,4891],{"class":414},"\"alembic_migration_lock\"",[284,4893,4894],{"class":300},").hexdigest()[:",[284,4896,60],{"class":426},[284,4898,4899],{"class":300},"], ",[284,4901,4902],{"class":426},"16",[284,4904,453],{"class":300},[284,4906,4907,4909,4912,4915,4917,4920,4923,4926],{"class":83,"line":405},[284,4908,4248],{"class":300},[284,4910,4911],{"class":296},"f",[284,4913,4914],{"class":414},"\"SELECT pg_advisory_lock(",[284,4916,1424],{"class":426},[284,4918,4919],{"class":300},"lock_key",[284,4921,4922],{"class":426},"}",[284,4924,4925],{"class":414},")\"",[284,4927,976],{"class":300},[284,4929,4930,4933],{"class":83,"line":418},[284,4931,4932],{"class":296},"    try",[284,4934,827],{"class":300},[284,4936,4937],{"class":83,"line":423},[284,4938,4939],{"class":300},"        context.configure(\n",[284,4941,4942,4945,4947],{"class":83,"line":456},[284,4943,4944],{"class":444},"            connection",[284,4946,411],{"class":296},[284,4948,1516],{"class":300},[284,4950,4951,4954,4956],{"class":83,"line":498},[284,4952,4953],{"class":444},"            target_metadata",[284,4955,411],{"class":296},[284,4957,1403],{"class":300},[284,4959,4960,4963,4965,4967],{"class":83,"line":509},[284,4961,4962],{"class":444},"            compare_type",[284,4964,411],{"class":296},[284,4966,450],{"class":426},[284,4968,869],{"class":300},[284,4970,4971,4974,4976,4978],{"class":83,"line":532},[284,4972,4973],{"class":444},"            compare_server_default",[284,4975,411],{"class":296},[284,4977,450],{"class":426},[284,4979,869],{"class":300},[284,4981,4982],{"class":83,"line":902},[284,4983,994],{"class":300},[284,4985,4986,4989],{"class":83,"line":907},[284,4987,4988],{"class":296},"        with",[284,4990,1470],{"class":300},[284,4992,4993],{"class":83,"line":912},[284,4994,4995],{"class":300},"            context.run_migrations()\n",[284,4997,4998,5001],{"class":83,"line":918},[284,4999,5000],{"class":296},"    finally",[284,5002,827],{"class":300},[284,5004,5005,5008,5010,5013,5015,5017,5019,5021],{"class":83,"line":929},[284,5006,5007],{"class":300},"        connection.execute(sa.text(",[284,5009,4911],{"class":296},[284,5011,5012],{"class":414},"\"SELECT pg_advisory_unlock(",[284,5014,1424],{"class":426},[284,5016,4919],{"class":300},[284,5018,4922],{"class":426},[284,5020,4925],{"class":414},[284,5022,976],{"class":300},[14,5024,5025],{},"The advisory lock is session-scoped: if the migration process crashes, PostgreSQL automatically releases the lock when the connection closes, preventing deadlocks.",[250,5027,5029],{"id":5028},"common-pitfalls-anti-patterns","Common Pitfalls & Anti-Patterns",[569,5031,5032,5067,5082,5109,5132,5146,5164],{},[572,5033,5034,5039,5040,5043,5044,3350,5047,3610,5050,5053,5054,5056,5057,5059,5060,5062,5063,5066],{},[263,5035,5036],{},[24,5037,5038],{},"Can't load plugin: sqlalchemy.dialects:postgres"," — You specified ",[24,5041,5042],{},"postgres:\u002F\u002F"," instead of ",[24,5045,5046],{},"postgresql:\u002F\u002F",[24,5048,5049],{},"postgresql+asyncpg:\u002F\u002F",[24,5051,5052],{},"sqlalchemy.url",". The ",[24,5055,5042],{}," scheme was removed in SQLAlchemy 1.4. Fix: update ",[24,5058,3613],{}," to use ",[24,5061,5049],{}," and ensure the ",[24,5064,5065],{},"asyncpg"," package is installed.",[572,5068,5069,5074,5075,5078,5079,5081],{},[263,5070,5071],{},[24,5072,5073],{},"Target database is not up to date"," — Alembic refuses to run ",[24,5076,5077],{},"revision --autogenerate"," when the database is behind the current head. Fix: run ",[24,5080,610],{}," first to bring the database to the latest revision before generating a new one. In CI, this error indicates a missing migration step in the pipeline.",[572,5083,5084,5089,5090,5092,5093,3350,5095,5097,5098,5101,5102,5105,5106,5108],{},[263,5085,5086],{},[24,5087,5088],{},"MissingGreenlet: greenlet_spawn has not been called"," — You are calling an async SQLAlchemy ORM method from synchronous Alembic migration code without using ",[24,5091,1187],{},". Fix: ensure all ORM calls inside ",[24,5094,594],{},[24,5096,600],{}," use ",[24,5099,5100],{},"op.execute(sa.text(...))"," (pure SQL via Core) rather than ",[24,5103,5104],{},"AsyncSession"," or async ORM methods. If you need ORM-level data manipulation, use a synchronous session obtained inside a ",[24,5107,130],{}," callback.",[572,5110,5111,5114,5115,5117,5118,5053,5120,5122,5123,5125,5126,5128,5129,5131],{},[263,5112,5113],{},"Autogenerate produces an empty migration"," — The most common cause is that model files are not imported before ",[24,5116,30],{}," sets ",[24,5119,95],{},[24,5121,265],{}," object only knows about tables whose ORM classes have been instantiated. Fix: add explicit imports of all model modules in ",[24,5124,30],{}," or in a centralized ",[24,5127,2024],{}," that ",[24,5130,30],{}," imports.",[572,5133,5134,5137,5138,5141,5142,5145],{},[263,5135,5136],{},"Autogenerate drops tables it should not"," — A variation of the missing-import problem. Fix: audit ",[24,5139,5140],{},"target_metadata.tables"," in a Python shell to confirm it contains all expected tables before running autogenerate. Also check ",[24,5143,5144],{},"include_schemas"," if you use non-default schemas.",[572,5147,5148,5153,5154,5156,5157,5160,5161,5163],{},[263,5149,5150],{},[24,5151,5152],{},"sqlalchemy.exc.InvalidRequestError: Can't reconnect until invalid transaction is rolled back"," — A previous DDL statement failed and left the connection in an aborted transaction state. This typically happens when ",[24,5155,3076],{}," is attempted inside a transaction. Fix: call ",[24,5158,5159],{},"op.execute(\"COMMIT\")"," before any concurrent DDL and configure the migration connection with ",[24,5162,3250],{}," for that specific operation.",[572,5165,5166,5171,5172,5175],{},[263,5167,5168],{},[24,5169,5170],{},"FAILED: Multiple head revisions are present for given argument 'head'"," — Two developers generated revisions from the same head simultaneously, creating a branch. Fix: run ",[24,5173,5174],{},"alembic merge heads -m \"merge_feature_branches\""," to create a merge revision that reconciles both branches.",[250,5177,5179],{"id":5178},"frequently-asked-questions","Frequently Asked Questions",[14,5181,5182,5185,5186,5188],{},[263,5183,5184],{},"Can I use Alembic's async env.py with FastAPI's lifespan event?","\nYes, but it is not recommended. Running ",[24,5187,610],{}," inside a FastAPI lifespan event makes your application startup dependent on migration success and serializes startup across replicas. The preferred pattern is to run migrations as a separate Kubernetes Job or Fargate task before rolling out new application instances, so that the application only starts after the schema is confirmed up to date.",[14,5190,5191,5198,5199,5202,5203,5206,5207,5210,5211,5214],{},[263,5192,5193,5194,5197],{},"Why does ",[24,5195,5196],{},"asyncio.run(run_migrations_online())"," fail with \"This event loop is already running\"?","\nThis error occurs when Alembic is invoked inside an already-running event loop — for example, inside a Jupyter notebook or a test that uses ",[24,5200,5201],{},"pytest-asyncio"," with a module-scoped loop. Fix: replace ",[24,5204,5205],{},"asyncio.run()"," with ",[24,5208,5209],{},"anyio.from_thread.run_sync()"," or ensure that migrations are always invoked as a subprocess (",[24,5212,5213],{},"subprocess.run([\"alembic\", \"upgrade\", \"head\"])",") rather than being called directly from async application code.",[14,5216,5217,5220,5221,5224,5225,3350,5227,5230,5231,5053,5234,5237],{},[263,5218,5219],{},"Does autogenerate detect changes to PostgreSQL ENUM types?","\nBy default, Alembic does not detect ",[24,5222,5223],{},"ENUM"," type changes because enums are shared across tables and their alteration is complex. You must implement a custom ",[24,5226,4082],{},[24,5228,5229],{},"process_revision_directives"," hook, or manage ENUM migrations manually with ",[24,5232,5233],{},"op.execute(\"ALTER TYPE ...\")",[24,5235,5236],{},"alembic-utils"," third-party package provides improved ENUM change detection.",[14,5239,5240,5243,5244,3610,5247,5249,5250,5253,5254,5257],{},[263,5241,5242],{},"How do I run migrations against a test database that uses aiosqlite?","\nSet ",[24,5245,5246],{},"sqlalchemy.url = sqlite+aiosqlite:\u002F\u002F\u002F.\u002Ftest.db",[24,5248,3613],{}," (or override via environment variable) and enable ",[24,5251,5252],{},"render_as_batch = true"," in the ",[24,5255,5256],{},"[alembic]"," section so that all column alterations use the batch recreation strategy compatible with SQLite's limited DDL support.",[14,5259,5260,5263,5264,5266,5267,5270,5271,5273,5274,5276,5277,485,5280,5283],{},[263,5261,5262],{},"What happens if a migration fails halfway through?","\nAlembic wraps each revision's ",[24,5265,594],{}," in a transaction by default (",[24,5268,5269],{},"with context.begin_transaction()","). If the revision raises an exception, the transaction is rolled back and ",[24,5272,154],{}," is not updated, leaving the database in the pre-migration state. For DDL statements that cannot run inside a transaction (concurrent index creation, some PostgreSQL-specific operations), you must handle partial failure manually — typically by writing the ",[24,5275,600],{}," to be idempotent (",[24,5278,5279],{},"DROP INDEX IF EXISTS",[24,5281,5282],{},"DROP COLUMN IF EXISTS",").",[14,5285,5286,5289,5290,5292,5293,5295,5296,5298,5299,5302,5303,5306,5307,631],{},[263,5287,5288],{},"How do I prevent autogenerate from including PostGIS or extension tables?","\nImplement an ",[24,5291,4082],{}," callback in ",[24,5294,30],{}," that returns ",[24,5297,493],{}," for tables you want to exclude, and pass it to ",[24,5300,5301],{},"context.configure(include_object=include_object)",". For extension schemas, also set ",[24,5304,5305],{},"include_schemas=False"," or explicitly list only the schemas you manage with ",[24,5308,5309],{},"include_schemas=[\"public\", \"app\"]",[250,5311,5313],{"id":5312},"related","Related",[569,5315,5316,5325,5331,5337,5343],{},[572,5317,5318,5321,5322,5324],{},[18,5319,5320],{"href":1191},"Configuring Alembic with Async SQLAlchemy Engines"," — Deep walkthrough of ",[24,5323,30],{}," async setup, credential injection, and asyncpg-specific options.",[572,5326,5327,5330],{},[18,5328,5329],{"href":3604},"Autogenerating and Reviewing Migration Scripts"," — How to audit autogenerated revisions, configure type comparison, and handle custom types.",[572,5332,5333,5336],{},[18,5334,5335],{"href":3060},"Zero-Downtime Schema Migration Strategies"," — Expand\u002Fcontract, concurrent index creation, and NOT NULL column patterns for live production databases.",[572,5338,5339,5342],{},[18,5340,5341],{"href":20},"Async Engines, Dialects, and Connection Pooling"," — Engine configuration, pool sizing, and driver selection that underpins your migration engine setup.",[572,5344,5345,5348,5349,485,5351,5353],{},[18,5346,5347],{"href":551},"SQLAlchemy 2.0 Core and ORM Architecture"," — Understanding ",[24,5350,81],{},[24,5352,265],{},", and the ORM mapping layer that Alembic introspects.",[5355,5356,5357],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":280,"searchDepth":293,"depth":293,"links":5359},[5360,5366,5370,5376,5382,5392,5393,5394],{"id":252,"depth":293,"text":253,"children":5361},[5362,5363,5364,5365],{"id":257,"depth":310,"text":258},{"id":559,"depth":310,"text":560},{"id":634,"depth":310,"text":635},{"id":750,"depth":310,"text":751},{"id":1169,"depth":293,"text":1170,"children":5367},[5368,5369],{"id":1173,"depth":310,"text":1174},{"id":1730,"depth":310,"text":1731},{"id":2001,"depth":293,"text":2002,"children":5371},[5372,5373,5374,5375],{"id":2005,"depth":310,"text":2006},{"id":2152,"depth":310,"text":2153},{"id":2301,"depth":310,"text":2302},{"id":2564,"depth":310,"text":2565},{"id":2602,"depth":293,"text":2603,"children":5377},[5378,5379,5380,5381],{"id":2606,"depth":310,"text":2607},{"id":2904,"depth":310,"text":2905},{"id":3065,"depth":310,"text":3066},{"id":3342,"depth":310,"text":3343},{"id":3617,"depth":293,"text":3618,"children":5383},[5384,5385,5386,5387,5388,5389,5390,5391],{"id":3621,"depth":310,"text":3622},{"id":3886,"depth":310,"text":3887},{"id":3964,"depth":310,"text":3965},{"id":4213,"depth":310,"text":4214},{"id":4319,"depth":310,"text":4320},{"id":4385,"depth":310,"text":4386},{"id":4500,"depth":310,"text":4501},{"id":4830,"depth":310,"text":4831},{"id":5028,"depth":293,"text":5029},{"id":5178,"depth":293,"text":5179},{"id":5312,"depth":293,"text":5313},"Alembic is the canonical migration tool for SQLAlchemy projects, managing every schema change from initial table creation to complex multi-step zero-downtime alterations — and when your application uses async engines and connection pooling, configuring Alembic correctly to cooperate with asyncio requires specific patterns that differ substantially from the synchronous default setup. This guide covers the complete migration lifecycle: wiring Alembic's env.py to an async engine, generating and auditing revision scripts, executing zero-downtime schema changes in production, and hardening your migration pipeline for CI\u002FCD deployment.","md",{"date":5398},"2026-06-18","\u002Falembic-async-migrations-and-schema-evolution",{"title":5,"description":5395},"alembic-async-migrations-and-schema-evolution\u002Findex","GTxyFgKFyuT_9X7B4p9tEP1W3u6YomXnHxr8uhBeGFc",1781810028982]