[{"data":1,"prerenderedAt":4185},["ShallowReactive",2],{"page-\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002F":3},{"id":4,"title":5,"body":6,"description":4177,"extension":4178,"meta":4179,"navigation":385,"path":4181,"seo":4182,"stem":4183,"__hash__":4184},"content\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002Findex.md","Configuring Alembic with Async SQLAlchemy Engines",{"type":7,"value":8,"toc":4143},"minimark",[9,13,32,37,48,51,80,93,102,263,270,275,278,313,330,334,355,415,648,848,861,970,974,986,1081,1088,1130,1137,1144,1148,1151,1309,1313,1316,1731,1735,1741,1771,1777,2343,2357,2361,2365,2382,2385,2403,2407,2429,2531,2538,2542,2556,2562,2566,2570,2591,2809,2813,2819,3180,3187,3191,3197,3430,3451,3455,3578,3590,3594,3598,3601,3667,3682,3686,3693,3708,3716,3720,3735,3822,3826,3960,3964,3972,3988,3999,4022,4030,4050,4058,4077,4086,4105,4109,4139],[10,11,5],"h1",{"id":12},"configuring-alembic-with-async-sqlalchemy-engines",[14,15,16,17,21,22,25,26,31],"p",{},"Configuring Alembic to drive schema migrations through an async SQLAlchemy engine requires a specific wiring pattern that bridges Alembic's inherently synchronous migration runner with the asyncio event loop. This guide covers the complete async ",[18,19,20],"code",{},"env.py"," structure, the ",[18,23,24],{},"run_sync"," bridging technique, environment-based URL configuration, and both online and offline migration modes — all within the broader ",[27,28,30],"a",{"href":29},"\u002Falembic-async-migrations-and-schema-evolution\u002F","Alembic Async Migrations and Schema Evolution"," workflow.",[33,34,36],"h2",{"id":35},"concept-and-execution-model","Concept and Execution Model",[14,38,39,40,43,44,47],{},"Alembic was designed around synchronous DBAPI connections. Its internal migration engine — the ",[18,41,42],{},"MigrationContext"," — expects a plain synchronous ",[18,45,46],{},"Connection"," object that it can call DDL through in a blocking fashion. SQLAlchemy 2.0's async engine does not expose such a connection directly; it wraps everything in an event-loop-aware async context manager.",[14,49,50],{},"The reconciliation is a two-step bridge:",[52,53,54,62],"ol",{},[55,56,57,58,61],"li",{},"An ",[18,59,60],{},"asyncio.run()"," call starts a dedicated event loop for the migration process.",[55,63,64,65,68,69,72,73,76,77,79],{},"Inside that coroutine, you acquire an ",[18,66,67],{},"AsyncConnection"," from a ",[18,70,71],{},"create_async_engine",", then invoke ",[18,74,75],{},"connection.run_sync(...)"," to hand Alembic a true synchronous ",[18,78,46],{}," object that it can use without any modifications to Alembic's internals.",[14,81,82,83,85,86,88,89,92],{},"This approach is officially supported and keeps your migration tooling free of monkey-patching or third-party wrappers. Alembic itself never sees the async layer; it operates against a synchronous facade that ",[18,84,24],{}," provides. The result is full compatibility with every Alembic feature — autogenerate, branching, batch operations, and custom migration templates — while your application code continues to use ",[18,87,71],{}," and ",[18,90,91],{},"AsyncSession"," at runtime.",[14,94,95,96,98,99,101],{},"Understanding this execution model matters before wiring ",[18,97,20],{},". The async engine you create inside ",[18,100,20],{}," is a short-lived migration-time engine, entirely separate from the engine your application uses at request time. The two can share the same URL; they must not share the same instance, since connection pool state is not safe to transfer across process or event-loop boundaries.",[103,104,107],"figure",{"className":105},[106],"diagram",[108,109,115,116,115,120,115,115,124,115,115,134,115,142,115,148,115,115,154,115,115,162,115,164,115,168,115,115,170,115,115,174,115,176,115,179,115,115,182,115,115,186,115,192,115,197,115,201,115,205,115,115,209,115,115,213,115,216,115,220,115,223,228,231,234,115,238,243,115,115,247],"svg",{"viewBox":110,"role":111,"ariaLabel":112,"xmlns":113,"style":114},"0 0 780 320","img","Async env.py execution flow from asyncio event loop through async engine and run_sync into Alembic migration context and DDL","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","max-width:100%;font-family:system-ui,sans-serif;","\n  ",[117,118,119],"title",{},"Async env.py execution flow",[121,122,123],"desc",{},"Diagram showing how asyncio.run() invokes an async coroutine, which creates an async engine, acquires an async connection, calls run_sync to obtain a synchronous connection, passes it to the Alembic MigrationContext, and finally emits DDL statements to the database.",[125,126],"rect",{"x":127,"y":127,"width":128,"height":129,"rx":130,"fill":131,"stroke":132,"style":133},"0","780","320","12","#f1f9f6","rgba(15,118,110,0.28)","stroke-width:1.5",[125,135],{"x":136,"y":137,"width":138,"height":139,"rx":140,"fill":141,"stroke":132,"style":133},"24","120","140","56","8","#113f39",[143,144,60],"text",{"x":145,"y":146,"fill":131,"style":147},"94","143","text-anchor:middle;font-size:12px;font-weight:600",[143,149,153],{"x":145,"y":150,"fill":151,"style":152},"161","#1f9f95","text-anchor:middle;font-size:11px","event loop",[155,156],"line",{"x1":157,"y1":158,"x2":159,"y2":158,"stroke":160,"style":161},"164","148","198","#0f766e","stroke-width:2;marker-end:url(#arrow)",[125,163],{"x":159,"y":137,"width":158,"height":139,"rx":140,"fill":141,"stroke":132,"style":133},[143,165,167],{"x":166,"y":146,"fill":131,"style":147},"272","AsyncEngine",[143,169,71],{"x":166,"y":150,"fill":151,"style":152},[155,171],{"x1":172,"y1":158,"x2":173,"y2":158,"stroke":160,"style":161},"346","380",[125,175],{"x":173,"y":137,"width":138,"height":139,"rx":140,"fill":160,"stroke":132,"style":133},[143,177,24],{"x":178,"y":146,"fill":131,"style":147},"450",[143,180,181],{"x":178,"y":150,"fill":131,"style":152},"sync Connection",[155,183],{"x1":184,"y1":158,"x2":185,"y2":158,"stroke":160,"style":161},"520","554",[125,187],{"x":188,"y":189,"width":190,"height":191,"rx":140,"fill":151,"stroke":132,"style":133},"548","106","162","84",[143,193,196],{"x":194,"y":195,"fill":131,"style":147},"629","133","Migration",[143,198,200],{"x":194,"y":199,"fill":131,"style":147},"149","Context",[143,202,204],{"x":194,"y":203,"fill":131,"style":152},"167","context.configure()",[143,206,208],{"x":194,"y":207,"fill":131,"style":152},"181","run_migrations()",[155,210],{"x1":194,"y1":211,"x2":194,"y2":212,"stroke":160,"style":161},"190","232",[125,214],{"x":188,"y":212,"width":190,"height":215,"rx":140,"fill":141,"stroke":132,"style":133},"52",[143,217,219],{"x":194,"y":218,"fill":131,"style":147},"255","PostgreSQL",[143,221,222],{"x":194,"y":166,"fill":151,"style":152},"DDL executed",[143,224,227],{"x":145,"y":225,"fill":226,"style":152},"110","#3f4f4b","\n1. Start loop\n",[143,229,230],{"x":166,"y":225,"fill":226,"style":152},"\n2. Create engine\n",[143,232,233],{"x":178,"y":225,"fill":226,"style":152},"\n3. Bridge sync\n",[143,235,237],{"x":194,"y":236,"fill":226,"style":152},"96","\n4. Configure & run\n",[143,239,242],{"x":240,"y":241,"fill":226,"style":152},"390","293","\nAlembic never sees the async layer —\n",[143,244,246],{"x":240,"y":245,"fill":226,"style":152},"309","\nrun_sync provides a synchronous facade\n",[248,249,250,251,115],"defs",{},"\n    ",[252,253,258,259,250],"marker",{"id":254,"markerWidth":140,"markerHeight":140,"refX":255,"refY":256,"orient":257},"arrow","6","3","auto","\n      ",[260,261],"path",{"d":262,"fill":160},"M0,0 L0,6 L8,3 z",[33,264,266,267,269],{"id":265},"async-engine-setup-and-envpy-structure","Async Engine Setup and ",[18,268,20],{}," Structure",[271,272,274],"h3",{"id":273},"installing-dependencies","Installing dependencies",[14,276,277],{},"Alembic, asyncpg, and SQLAlchemy must be present before any configuration takes place:",[279,280,285],"pre",{"className":281,"code":282,"language":283,"meta":284,"style":284},"language-python shiki shiki-themes github-light github-dark","# requirements.txt or pyproject.toml dependencies\n# alembic>=1.13.0\n# sqlalchemy[asyncio]>=2.0.0\n# asyncpg>=0.29.0\n","python","",[18,286,287,295,301,307],{"__ignoreMap":284},[288,289,291],"span",{"class":155,"line":290},1,[288,292,294],{"class":293},"sJ8bj","# requirements.txt or pyproject.toml dependencies\n",[288,296,298],{"class":155,"line":297},2,[288,299,300],{"class":293},"# alembic>=1.13.0\n",[288,302,304],{"class":155,"line":303},3,[288,305,306],{"class":293},"# sqlalchemy[asyncio]>=2.0.0\n",[288,308,310],{"class":155,"line":309},4,[288,311,312],{"class":293},"# asyncpg>=0.29.0\n",[14,314,315,316,319,320,88,323,326,327,329],{},"Initialize a fresh Alembic environment with ",[18,317,318],{},"alembic init alembic",". This creates ",[18,321,322],{},"alembic.ini",[18,324,325],{},"alembic\u002Fenv.py",". The generated ",[18,328,20],{}," is synchronous; you will rewrite it entirely.",[271,331,333],{"id":332},"the-application-metadata-contract","The application metadata contract",[14,335,336,337,340,341,344,345,347,348,351,352,354],{},"Alembic's autogenerate feature compares the current database schema against your ",[18,338,339],{},"DeclarativeBase"," metadata. The binding point is ",[18,342,343],{},"target_metadata"," in ",[18,346,20],{},". Your application's model file must expose its ",[18,349,350],{},"Base.metadata"," object, and ",[18,353,20],{}," imports it directly:",[279,356,358],{"className":281,"code":357,"language":283,"meta":284,"style":284},"# app\u002Fmodels\u002Fbase.py\nfrom sqlalchemy.orm import DeclarativeBase\n\n\nclass Base(DeclarativeBase):\n    pass\n",[18,359,360,365,381,387,391,409],{"__ignoreMap":284},[288,361,362],{"class":155,"line":290},[288,363,364],{"class":293},"# app\u002Fmodels\u002Fbase.py\n",[288,366,367,371,375,378],{"class":155,"line":297},[288,368,370],{"class":369},"szBVR","from",[288,372,374],{"class":373},"sVt8B"," sqlalchemy.orm ",[288,376,377],{"class":369},"import",[288,379,380],{"class":373}," DeclarativeBase\n",[288,382,383],{"class":155,"line":303},[288,384,386],{"emptyLinePlaceholder":385},true,"\n",[288,388,389],{"class":155,"line":309},[288,390,386],{"emptyLinePlaceholder":385},[288,392,394,397,401,404,406],{"class":155,"line":393},5,[288,395,396],{"class":369},"class",[288,398,400],{"class":399},"sScJk"," Base",[288,402,403],{"class":373},"(",[288,405,339],{"class":399},[288,407,408],{"class":373},"):\n",[288,410,412],{"class":155,"line":411},6,[288,413,414],{"class":369},"    pass\n",[279,416,418],{"className":281,"code":417,"language":283,"meta":284,"style":284},"# app\u002Fmodels\u002Fuser.py\nfrom sqlalchemy import String, DateTime, func\nfrom sqlalchemy.orm import Mapped, mapped_column\nfrom .base import Base\n\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)\n    full_name: Mapped[str] = mapped_column(String(255), nullable=False)\n    created_at: Mapped[DateTime] = mapped_column(\n        DateTime(timezone=True), server_default=func.now(), nullable=False\n    )\n",[18,419,420,425,437,448,460,464,468,483,496,501,534,575,601,612,642],{"__ignoreMap":284},[288,421,422],{"class":155,"line":290},[288,423,424],{"class":293},"# app\u002Fmodels\u002Fuser.py\n",[288,426,427,429,432,434],{"class":155,"line":297},[288,428,370],{"class":369},[288,430,431],{"class":373}," sqlalchemy ",[288,433,377],{"class":369},[288,435,436],{"class":373}," String, DateTime, func\n",[288,438,439,441,443,445],{"class":155,"line":303},[288,440,370],{"class":369},[288,442,374],{"class":373},[288,444,377],{"class":369},[288,446,447],{"class":373}," Mapped, mapped_column\n",[288,449,450,452,455,457],{"class":155,"line":309},[288,451,370],{"class":369},[288,453,454],{"class":373}," .base ",[288,456,377],{"class":369},[288,458,459],{"class":373}," Base\n",[288,461,462],{"class":155,"line":393},[288,463,386],{"emptyLinePlaceholder":385},[288,465,466],{"class":155,"line":411},[288,467,386],{"emptyLinePlaceholder":385},[288,469,471,473,476,478,481],{"class":155,"line":470},7,[288,472,396],{"class":369},[288,474,475],{"class":399}," User",[288,477,403],{"class":373},[288,479,480],{"class":399},"Base",[288,482,408],{"class":373},[288,484,486,489,492],{"class":155,"line":485},8,[288,487,488],{"class":373},"    __tablename__ ",[288,490,491],{"class":369},"=",[288,493,495],{"class":494},"sZZnC"," \"users\"\n",[288,497,499],{"class":155,"line":498},9,[288,500,386],{"emptyLinePlaceholder":385},[288,502,504,508,511,514,517,519,522,526,528,531],{"class":155,"line":503},10,[288,505,507],{"class":506},"sj4cs","    id",[288,509,510],{"class":373},": Mapped[",[288,512,513],{"class":506},"int",[288,515,516],{"class":373},"] ",[288,518,491],{"class":369},[288,520,521],{"class":373}," mapped_column(",[288,523,525],{"class":524},"s4XuR","primary_key",[288,527,491],{"class":369},[288,529,530],{"class":506},"True",[288,532,533],{"class":373},")\n",[288,535,537,540,543,545,547,550,552,555,558,560,562,565,568,570,573],{"class":155,"line":536},11,[288,538,539],{"class":373},"    email: Mapped[",[288,541,542],{"class":506},"str",[288,544,516],{"class":373},[288,546,491],{"class":369},[288,548,549],{"class":373}," mapped_column(String(",[288,551,218],{"class":506},[288,553,554],{"class":373},"), ",[288,556,557],{"class":524},"unique",[288,559,491],{"class":369},[288,561,530],{"class":506},[288,563,564],{"class":373},", ",[288,566,567],{"class":524},"nullable",[288,569,491],{"class":369},[288,571,572],{"class":506},"False",[288,574,533],{"class":373},[288,576,578,581,583,585,587,589,591,593,595,597,599],{"class":155,"line":577},12,[288,579,580],{"class":373},"    full_name: Mapped[",[288,582,542],{"class":506},[288,584,516],{"class":373},[288,586,491],{"class":369},[288,588,549],{"class":373},[288,590,218],{"class":506},[288,592,554],{"class":373},[288,594,567],{"class":524},[288,596,491],{"class":369},[288,598,572],{"class":506},[288,600,533],{"class":373},[288,602,604,607,609],{"class":155,"line":603},13,[288,605,606],{"class":373},"    created_at: Mapped[DateTime] ",[288,608,491],{"class":369},[288,610,611],{"class":373}," mapped_column(\n",[288,613,615,618,621,623,625,627,630,632,635,637,639],{"class":155,"line":614},14,[288,616,617],{"class":373},"        DateTime(",[288,619,620],{"class":524},"timezone",[288,622,491],{"class":369},[288,624,530],{"class":506},[288,626,554],{"class":373},[288,628,629],{"class":524},"server_default",[288,631,491],{"class":369},[288,633,634],{"class":373},"func.now(), ",[288,636,567],{"class":524},[288,638,491],{"class":369},[288,640,641],{"class":506},"False\n",[288,643,645],{"class":155,"line":644},15,[288,646,647],{"class":373},"    )\n",[279,649,651],{"className":281,"code":650,"language":283,"meta":284,"style":284},"# app\u002Fmodels\u002Forder.py\nfrom decimal import Decimal\nfrom sqlalchemy import Numeric, ForeignKey, String\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship\nfrom .base import Base\n\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int] = mapped_column(ForeignKey(\"users.id\"), nullable=False)\n    status: Mapped[str] = mapped_column(String(32), nullable=False, default=\"pending\")\n    total: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False)\n",[18,652,653,658,670,681,692,702,706,710,723,732,736,758,785,821],{"__ignoreMap":284},[288,654,655],{"class":155,"line":290},[288,656,657],{"class":293},"# app\u002Fmodels\u002Forder.py\n",[288,659,660,662,665,667],{"class":155,"line":297},[288,661,370],{"class":369},[288,663,664],{"class":373}," decimal ",[288,666,377],{"class":369},[288,668,669],{"class":373}," Decimal\n",[288,671,672,674,676,678],{"class":155,"line":303},[288,673,370],{"class":369},[288,675,431],{"class":373},[288,677,377],{"class":369},[288,679,680],{"class":373}," Numeric, ForeignKey, String\n",[288,682,683,685,687,689],{"class":155,"line":309},[288,684,370],{"class":369},[288,686,374],{"class":373},[288,688,377],{"class":369},[288,690,691],{"class":373}," Mapped, mapped_column, relationship\n",[288,693,694,696,698,700],{"class":155,"line":393},[288,695,370],{"class":369},[288,697,454],{"class":373},[288,699,377],{"class":369},[288,701,459],{"class":373},[288,703,704],{"class":155,"line":411},[288,705,386],{"emptyLinePlaceholder":385},[288,707,708],{"class":155,"line":470},[288,709,386],{"emptyLinePlaceholder":385},[288,711,712,714,717,719,721],{"class":155,"line":485},[288,713,396],{"class":369},[288,715,716],{"class":399}," Order",[288,718,403],{"class":373},[288,720,480],{"class":399},[288,722,408],{"class":373},[288,724,725,727,729],{"class":155,"line":498},[288,726,488],{"class":373},[288,728,491],{"class":369},[288,730,731],{"class":494}," \"orders\"\n",[288,733,734],{"class":155,"line":503},[288,735,386],{"emptyLinePlaceholder":385},[288,737,738,740,742,744,746,748,750,752,754,756],{"class":155,"line":536},[288,739,507],{"class":506},[288,741,510],{"class":373},[288,743,513],{"class":506},[288,745,516],{"class":373},[288,747,491],{"class":369},[288,749,521],{"class":373},[288,751,525],{"class":524},[288,753,491],{"class":369},[288,755,530],{"class":506},[288,757,533],{"class":373},[288,759,760,763,765,767,769,772,775,777,779,781,783],{"class":155,"line":577},[288,761,762],{"class":373},"    user_id: Mapped[",[288,764,513],{"class":506},[288,766,516],{"class":373},[288,768,491],{"class":369},[288,770,771],{"class":373}," mapped_column(ForeignKey(",[288,773,774],{"class":494},"\"users.id\"",[288,776,554],{"class":373},[288,778,567],{"class":524},[288,780,491],{"class":369},[288,782,572],{"class":506},[288,784,533],{"class":373},[288,786,787,790,792,794,796,798,801,803,805,807,809,811,814,816,819],{"class":155,"line":603},[288,788,789],{"class":373},"    status: Mapped[",[288,791,542],{"class":506},[288,793,516],{"class":373},[288,795,491],{"class":369},[288,797,549],{"class":373},[288,799,800],{"class":506},"32",[288,802,554],{"class":373},[288,804,567],{"class":524},[288,806,491],{"class":369},[288,808,572],{"class":506},[288,810,564],{"class":373},[288,812,813],{"class":524},"default",[288,815,491],{"class":369},[288,817,818],{"class":494},"\"pending\"",[288,820,533],{"class":373},[288,822,823,826,828,831,833,835,838,840,842,844,846],{"class":155,"line":614},[288,824,825],{"class":373},"    total: Mapped[Decimal] ",[288,827,491],{"class":369},[288,829,830],{"class":373}," mapped_column(Numeric(",[288,832,130],{"class":506},[288,834,564],{"class":373},[288,836,837],{"class":506},"2",[288,839,554],{"class":373},[288,841,567],{"class":524},[288,843,491],{"class":369},[288,845,572],{"class":506},[288,847,533],{"class":373},[14,849,850,851,853,854,856,857,860],{},"Import every model module before ",[18,852,20],{}," references ",[18,855,350],{},", otherwise Alembic will not detect tables defined in unimported modules. A common pattern is a single ",[18,858,859],{},"app\u002Fmodels\u002F__init__.py"," that imports each model:",[279,862,864],{"className":281,"code":863,"language":283,"meta":284,"style":284},"# app\u002Fmodels\u002F__init__.py\nfrom .base import Base\nfrom .user import User\nfrom .order import Order\nfrom .product import Product\nfrom .invoice import Invoice\n\n__all__ = [\"Base\", \"User\", \"Order\", \"Product\", \"Invoice\"]\n",[18,865,866,871,881,893,905,917,929,933],{"__ignoreMap":284},[288,867,868],{"class":155,"line":290},[288,869,870],{"class":293},"# app\u002Fmodels\u002F__init__.py\n",[288,872,873,875,877,879],{"class":155,"line":297},[288,874,370],{"class":369},[288,876,454],{"class":373},[288,878,377],{"class":369},[288,880,459],{"class":373},[288,882,883,885,888,890],{"class":155,"line":303},[288,884,370],{"class":369},[288,886,887],{"class":373}," .user ",[288,889,377],{"class":369},[288,891,892],{"class":373}," User\n",[288,894,895,897,900,902],{"class":155,"line":309},[288,896,370],{"class":369},[288,898,899],{"class":373}," .order ",[288,901,377],{"class":369},[288,903,904],{"class":373}," Order\n",[288,906,907,909,912,914],{"class":155,"line":393},[288,908,370],{"class":369},[288,910,911],{"class":373}," .product ",[288,913,377],{"class":369},[288,915,916],{"class":373}," Product\n",[288,918,919,921,924,926],{"class":155,"line":411},[288,920,370],{"class":369},[288,922,923],{"class":373}," .invoice ",[288,925,377],{"class":369},[288,927,928],{"class":373}," Invoice\n",[288,930,931],{"class":155,"line":470},[288,932,386],{"emptyLinePlaceholder":385},[288,934,935,938,941,944,947,949,952,954,957,959,962,964,967],{"class":155,"line":485},[288,936,937],{"class":506},"__all__",[288,939,940],{"class":369}," =",[288,942,943],{"class":373}," [",[288,945,946],{"class":494},"\"Base\"",[288,948,564],{"class":373},[288,950,951],{"class":494},"\"User\"",[288,953,564],{"class":373},[288,955,956],{"class":494},"\"Order\"",[288,958,564],{"class":373},[288,960,961],{"class":494},"\"Product\"",[288,963,564],{"class":373},[288,965,966],{"class":494},"\"Invoice\"",[288,968,969],{"class":373},"]\n",[271,971,973],{"id":972},"reading-the-database-url-from-the-environment","Reading the database URL from the environment",[14,975,976,977,979,980,982,983,985],{},"Hard-coding the database URL in ",[18,978,322],{}," works for single-environment setups but breaks in CI pipelines and multi-tenant deployments. The recommended pattern reads from environment variables and overrides the ",[18,981,322],{}," value at runtime inside ",[18,984,20],{},":",[279,987,989],{"className":281,"code":988,"language":283,"meta":284,"style":284},"# alembic\u002Fenv.py — URL configuration section\nimport os\nfrom alembic import context\n\n# alembic.ini still has a placeholder; we override it here\nDATABASE_URL = os.environ.get(\n    \"DATABASE_URL\",\n    \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fappdb\",\n)\n\nconfig = context.config\nconfig.set_main_option(\"sqlalchemy.url\", DATABASE_URL)\n",[18,990,991,996,1003,1015,1019,1024,1034,1042,1049,1053,1057,1067],{"__ignoreMap":284},[288,992,993],{"class":155,"line":290},[288,994,995],{"class":293},"# alembic\u002Fenv.py — URL configuration section\n",[288,997,998,1000],{"class":155,"line":297},[288,999,377],{"class":369},[288,1001,1002],{"class":373}," os\n",[288,1004,1005,1007,1010,1012],{"class":155,"line":303},[288,1006,370],{"class":369},[288,1008,1009],{"class":373}," alembic ",[288,1011,377],{"class":369},[288,1013,1014],{"class":373}," context\n",[288,1016,1017],{"class":155,"line":309},[288,1018,386],{"emptyLinePlaceholder":385},[288,1020,1021],{"class":155,"line":393},[288,1022,1023],{"class":293},"# alembic.ini still has a placeholder; we override it here\n",[288,1025,1026,1029,1031],{"class":155,"line":411},[288,1027,1028],{"class":506},"DATABASE_URL",[288,1030,940],{"class":369},[288,1032,1033],{"class":373}," os.environ.get(\n",[288,1035,1036,1039],{"class":155,"line":470},[288,1037,1038],{"class":494},"    \"DATABASE_URL\"",[288,1040,1041],{"class":373},",\n",[288,1043,1044,1047],{"class":155,"line":485},[288,1045,1046],{"class":494},"    \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fappdb\"",[288,1048,1041],{"class":373},[288,1050,1051],{"class":155,"line":498},[288,1052,533],{"class":373},[288,1054,1055],{"class":155,"line":503},[288,1056,386],{"emptyLinePlaceholder":385},[288,1058,1059,1062,1064],{"class":155,"line":536},[288,1060,1061],{"class":373},"config ",[288,1063,491],{"class":369},[288,1065,1066],{"class":373}," context.config\n",[288,1068,1069,1072,1075,1077,1079],{"class":155,"line":577},[288,1070,1071],{"class":373},"config.set_main_option(",[288,1073,1074],{"class":494},"\"sqlalchemy.url\"",[288,1076,564],{"class":373},[288,1078,1028],{"class":506},[288,1080,533],{"class":373},[14,1082,1083,1084,1087],{},"For applications that centralize settings through Pydantic or ",[18,1085,1086],{},"python-decouple",", call the settings object directly:",[279,1089,1091],{"className":281,"code":1090,"language":283,"meta":284,"style":284},"# Pydantic BaseSettings alternative\nfrom app.config import settings  # exposes settings.database_url\n\nconfig.set_main_option(\"sqlalchemy.url\", str(settings.database_url))\n",[18,1092,1093,1098,1113,1117],{"__ignoreMap":284},[288,1094,1095],{"class":155,"line":290},[288,1096,1097],{"class":293},"# Pydantic BaseSettings alternative\n",[288,1099,1100,1102,1105,1107,1110],{"class":155,"line":297},[288,1101,370],{"class":369},[288,1103,1104],{"class":373}," app.config ",[288,1106,377],{"class":369},[288,1108,1109],{"class":373}," settings  ",[288,1111,1112],{"class":293},"# exposes settings.database_url\n",[288,1114,1115],{"class":155,"line":303},[288,1116,386],{"emptyLinePlaceholder":385},[288,1118,1119,1121,1123,1125,1127],{"class":155,"line":309},[288,1120,1071],{"class":373},[288,1122,1074],{"class":494},[288,1124,564],{"class":373},[288,1126,542],{"class":506},[288,1128,1129],{"class":373},"(settings.database_url))\n",[14,1131,1132,1133,1136],{},"This keeps the migration command clean: ",[18,1134,1135],{},"DATABASE_URL=postgresql+asyncpg:\u002F\u002F... alembic upgrade head"," — no alembic.ini edits needed per environment.",[33,1138,1140,1141,1143],{"id":1139},"complete-async-envpy-implementation","Complete Async ",[18,1142,20],{}," Implementation",[271,1145,1147],{"id":1146},"offline-mode-sql-generation-without-a-live-connection","Offline mode (SQL generation without a live connection)",[14,1149,1150],{},"Offline mode generates SQL scripts for manual review or deployment through a change-management pipeline. It does not require a live database connection and remains fully synchronous:",[279,1152,1154],{"className":281,"code":1153,"language":283,"meta":284,"style":284},"def run_migrations_offline() -> None:\n    \"\"\"\n    Emit migration SQL to stdout without a live database connection.\n    Useful for generating reviewed scripts for production deployments.\n    \"\"\"\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\n    with context.begin_transaction():\n        context.run_migrations()\n",[18,1155,1156,1173,1178,1183,1188,1192,1206,1211,1221,1231,1242,1264,1275,1286,1290,1294,1303],{"__ignoreMap":284},[288,1157,1158,1161,1164,1167,1170],{"class":155,"line":290},[288,1159,1160],{"class":369},"def",[288,1162,1163],{"class":399}," run_migrations_offline",[288,1165,1166],{"class":373},"() -> ",[288,1168,1169],{"class":506},"None",[288,1171,1172],{"class":373},":\n",[288,1174,1175],{"class":155,"line":297},[288,1176,1177],{"class":494},"    \"\"\"\n",[288,1179,1180],{"class":155,"line":303},[288,1181,1182],{"class":494},"    Emit migration SQL to stdout without a live database connection.\n",[288,1184,1185],{"class":155,"line":309},[288,1186,1187],{"class":494},"    Useful for generating reviewed scripts for production deployments.\n",[288,1189,1190],{"class":155,"line":393},[288,1191,1177],{"class":494},[288,1193,1194,1197,1199,1202,1204],{"class":155,"line":411},[288,1195,1196],{"class":373},"    url ",[288,1198,491],{"class":369},[288,1200,1201],{"class":373}," config.get_main_option(",[288,1203,1074],{"class":494},[288,1205,533],{"class":373},[288,1207,1208],{"class":155,"line":470},[288,1209,1210],{"class":373},"    context.configure(\n",[288,1212,1213,1216,1218],{"class":155,"line":485},[288,1214,1215],{"class":524},"        url",[288,1217,491],{"class":369},[288,1219,1220],{"class":373},"url,\n",[288,1222,1223,1226,1228],{"class":155,"line":498},[288,1224,1225],{"class":524},"        target_metadata",[288,1227,491],{"class":369},[288,1229,1230],{"class":373},"target_metadata,\n",[288,1232,1233,1236,1238,1240],{"class":155,"line":503},[288,1234,1235],{"class":524},"        literal_binds",[288,1237,491],{"class":369},[288,1239,530],{"class":506},[288,1241,1041],{"class":373},[288,1243,1244,1247,1249,1252,1255,1258,1261],{"class":155,"line":536},[288,1245,1246],{"class":524},"        dialect_opts",[288,1248,491],{"class":369},[288,1250,1251],{"class":373},"{",[288,1253,1254],{"class":494},"\"paramstyle\"",[288,1256,1257],{"class":373},": ",[288,1259,1260],{"class":494},"\"named\"",[288,1262,1263],{"class":373},"},\n",[288,1265,1266,1269,1271,1273],{"class":155,"line":577},[288,1267,1268],{"class":524},"        compare_type",[288,1270,491],{"class":369},[288,1272,530],{"class":506},[288,1274,1041],{"class":373},[288,1276,1277,1280,1282,1284],{"class":155,"line":603},[288,1278,1279],{"class":524},"        compare_server_default",[288,1281,491],{"class":369},[288,1283,530],{"class":506},[288,1285,1041],{"class":373},[288,1287,1288],{"class":155,"line":614},[288,1289,647],{"class":373},[288,1291,1292],{"class":155,"line":644},[288,1293,386],{"emptyLinePlaceholder":385},[288,1295,1297,1300],{"class":155,"line":1296},16,[288,1298,1299],{"class":369},"    with",[288,1301,1302],{"class":373}," context.begin_transaction():\n",[288,1304,1306],{"class":155,"line":1305},17,[288,1307,1308],{"class":373},"        context.run_migrations()\n",[271,1310,1312],{"id":1311},"online-mode-async-bridge-pattern","Online mode (async bridge pattern)",[14,1314,1315],{},"Online mode executes migrations against a live database. The async bridge is the critical section:",[279,1317,1319],{"className":281,"code":1318,"language":283,"meta":284,"style":284},"import asyncio\nfrom logging.config import fileConfig\n\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection\nfrom alembic import context\n\n# Import your app's metadata so autogenerate can compare schemas\nfrom app.models import Base\n\ntarget_metadata = Base.metadata\n\nconfig = context.config\nfileConfig(config.config_file_name)  # type: ignore[arg-type]\n\n\ndef do_run_migrations(connection: AsyncConnection) -> None:\n    \"\"\"\n    Called inside run_sync — receives a plain synchronous Connection.\n    This is the synchronous half that Alembic's MigrationContext expects.\n    \"\"\"\n    context.configure(\n        connection=connection,  # type: ignore[arg-type]\n        target_metadata=target_metadata,\n        compare_type=True,\n        compare_server_default=True,\n        include_schemas=True,\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\nasync def run_async_migrations() -> None:\n    \"\"\"\n    Create a short-lived async engine, acquire a connection,\n    then delegate to the synchronous migration runner via run_sync.\n    \"\"\"\n    connectable = create_async_engine(\n        config.get_main_option(\"sqlalchemy.url\"),  # type: ignore[arg-type]\n        echo=False,\n        pool_pre_ping=True,\n    )\n\n    async with connectable.connect() as connection:\n        await connection.run_sync(do_run_migrations)\n\n    await connectable.dispose()\n\n\ndef run_migrations_online() -> None:\n    \"\"\"Entry point called by Alembic when a live connection is available.\"\"\"\n    asyncio.run(run_async_migrations())\n",[18,1320,1321,1328,1340,1344,1356,1366,1370,1375,1386,1390,1400,1404,1412,1420,1424,1428,1442,1446,1452,1458,1463,1468,1481,1490,1501,1512,1524,1529,1534,1541,1546,1551,1556,1574,1579,1585,1591,1596,1607,1620,1632,1644,1649,1654,1672,1681,1686,1695,1700,1705,1719,1725],{"__ignoreMap":284},[288,1322,1323,1325],{"class":155,"line":290},[288,1324,377],{"class":369},[288,1326,1327],{"class":373}," asyncio\n",[288,1329,1330,1332,1335,1337],{"class":155,"line":297},[288,1331,370],{"class":369},[288,1333,1334],{"class":373}," logging.config ",[288,1336,377],{"class":369},[288,1338,1339],{"class":373}," fileConfig\n",[288,1341,1342],{"class":155,"line":303},[288,1343,386],{"emptyLinePlaceholder":385},[288,1345,1346,1348,1351,1353],{"class":155,"line":309},[288,1347,370],{"class":369},[288,1349,1350],{"class":373}," sqlalchemy.ext.asyncio ",[288,1352,377],{"class":369},[288,1354,1355],{"class":373}," create_async_engine, AsyncConnection\n",[288,1357,1358,1360,1362,1364],{"class":155,"line":393},[288,1359,370],{"class":369},[288,1361,1009],{"class":373},[288,1363,377],{"class":369},[288,1365,1014],{"class":373},[288,1367,1368],{"class":155,"line":411},[288,1369,386],{"emptyLinePlaceholder":385},[288,1371,1372],{"class":155,"line":470},[288,1373,1374],{"class":293},"# Import your app's metadata so autogenerate can compare schemas\n",[288,1376,1377,1379,1382,1384],{"class":155,"line":485},[288,1378,370],{"class":369},[288,1380,1381],{"class":373}," app.models ",[288,1383,377],{"class":369},[288,1385,459],{"class":373},[288,1387,1388],{"class":155,"line":498},[288,1389,386],{"emptyLinePlaceholder":385},[288,1391,1392,1395,1397],{"class":155,"line":503},[288,1393,1394],{"class":373},"target_metadata ",[288,1396,491],{"class":369},[288,1398,1399],{"class":373}," Base.metadata\n",[288,1401,1402],{"class":155,"line":536},[288,1403,386],{"emptyLinePlaceholder":385},[288,1405,1406,1408,1410],{"class":155,"line":577},[288,1407,1061],{"class":373},[288,1409,491],{"class":369},[288,1411,1066],{"class":373},[288,1413,1414,1417],{"class":155,"line":603},[288,1415,1416],{"class":373},"fileConfig(config.config_file_name)  ",[288,1418,1419],{"class":293},"# type: ignore[arg-type]\n",[288,1421,1422],{"class":155,"line":614},[288,1423,386],{"emptyLinePlaceholder":385},[288,1425,1426],{"class":155,"line":644},[288,1427,386],{"emptyLinePlaceholder":385},[288,1429,1430,1432,1435,1438,1440],{"class":155,"line":1296},[288,1431,1160],{"class":369},[288,1433,1434],{"class":399}," do_run_migrations",[288,1436,1437],{"class":373},"(connection: AsyncConnection) -> ",[288,1439,1169],{"class":506},[288,1441,1172],{"class":373},[288,1443,1444],{"class":155,"line":1305},[288,1445,1177],{"class":494},[288,1447,1449],{"class":155,"line":1448},18,[288,1450,1451],{"class":494},"    Called inside run_sync — receives a plain synchronous Connection.\n",[288,1453,1455],{"class":155,"line":1454},19,[288,1456,1457],{"class":494},"    This is the synchronous half that Alembic's MigrationContext expects.\n",[288,1459,1461],{"class":155,"line":1460},20,[288,1462,1177],{"class":494},[288,1464,1466],{"class":155,"line":1465},21,[288,1467,1210],{"class":373},[288,1469,1471,1474,1476,1479],{"class":155,"line":1470},22,[288,1472,1473],{"class":524},"        connection",[288,1475,491],{"class":369},[288,1477,1478],{"class":373},"connection,  ",[288,1480,1419],{"class":293},[288,1482,1484,1486,1488],{"class":155,"line":1483},23,[288,1485,1225],{"class":524},[288,1487,491],{"class":369},[288,1489,1230],{"class":373},[288,1491,1493,1495,1497,1499],{"class":155,"line":1492},24,[288,1494,1268],{"class":524},[288,1496,491],{"class":369},[288,1498,530],{"class":506},[288,1500,1041],{"class":373},[288,1502,1504,1506,1508,1510],{"class":155,"line":1503},25,[288,1505,1279],{"class":524},[288,1507,491],{"class":369},[288,1509,530],{"class":506},[288,1511,1041],{"class":373},[288,1513,1515,1518,1520,1522],{"class":155,"line":1514},26,[288,1516,1517],{"class":524},"        include_schemas",[288,1519,491],{"class":369},[288,1521,530],{"class":506},[288,1523,1041],{"class":373},[288,1525,1527],{"class":155,"line":1526},27,[288,1528,647],{"class":373},[288,1530,1532],{"class":155,"line":1531},28,[288,1533,386],{"emptyLinePlaceholder":385},[288,1535,1537,1539],{"class":155,"line":1536},29,[288,1538,1299],{"class":369},[288,1540,1302],{"class":373},[288,1542,1544],{"class":155,"line":1543},30,[288,1545,1308],{"class":373},[288,1547,1549],{"class":155,"line":1548},31,[288,1550,386],{"emptyLinePlaceholder":385},[288,1552,1554],{"class":155,"line":1553},32,[288,1555,386],{"emptyLinePlaceholder":385},[288,1557,1559,1562,1565,1568,1570,1572],{"class":155,"line":1558},33,[288,1560,1561],{"class":369},"async",[288,1563,1564],{"class":369}," def",[288,1566,1567],{"class":399}," run_async_migrations",[288,1569,1166],{"class":373},[288,1571,1169],{"class":506},[288,1573,1172],{"class":373},[288,1575,1577],{"class":155,"line":1576},34,[288,1578,1177],{"class":494},[288,1580,1582],{"class":155,"line":1581},35,[288,1583,1584],{"class":494},"    Create a short-lived async engine, acquire a connection,\n",[288,1586,1588],{"class":155,"line":1587},36,[288,1589,1590],{"class":494},"    then delegate to the synchronous migration runner via run_sync.\n",[288,1592,1594],{"class":155,"line":1593},37,[288,1595,1177],{"class":494},[288,1597,1599,1602,1604],{"class":155,"line":1598},38,[288,1600,1601],{"class":373},"    connectable ",[288,1603,491],{"class":369},[288,1605,1606],{"class":373}," create_async_engine(\n",[288,1608,1610,1613,1615,1618],{"class":155,"line":1609},39,[288,1611,1612],{"class":373},"        config.get_main_option(",[288,1614,1074],{"class":494},[288,1616,1617],{"class":373},"),  ",[288,1619,1419],{"class":293},[288,1621,1623,1626,1628,1630],{"class":155,"line":1622},40,[288,1624,1625],{"class":524},"        echo",[288,1627,491],{"class":369},[288,1629,572],{"class":506},[288,1631,1041],{"class":373},[288,1633,1635,1638,1640,1642],{"class":155,"line":1634},41,[288,1636,1637],{"class":524},"        pool_pre_ping",[288,1639,491],{"class":369},[288,1641,530],{"class":506},[288,1643,1041],{"class":373},[288,1645,1647],{"class":155,"line":1646},42,[288,1648,647],{"class":373},[288,1650,1652],{"class":155,"line":1651},43,[288,1653,386],{"emptyLinePlaceholder":385},[288,1655,1657,1660,1663,1666,1669],{"class":155,"line":1656},44,[288,1658,1659],{"class":369},"    async",[288,1661,1662],{"class":369}," with",[288,1664,1665],{"class":373}," connectable.connect() ",[288,1667,1668],{"class":369},"as",[288,1670,1671],{"class":373}," connection:\n",[288,1673,1675,1678],{"class":155,"line":1674},45,[288,1676,1677],{"class":369},"        await",[288,1679,1680],{"class":373}," connection.run_sync(do_run_migrations)\n",[288,1682,1684],{"class":155,"line":1683},46,[288,1685,386],{"emptyLinePlaceholder":385},[288,1687,1689,1692],{"class":155,"line":1688},47,[288,1690,1691],{"class":369},"    await",[288,1693,1694],{"class":373}," connectable.dispose()\n",[288,1696,1698],{"class":155,"line":1697},48,[288,1699,386],{"emptyLinePlaceholder":385},[288,1701,1703],{"class":155,"line":1702},49,[288,1704,386],{"emptyLinePlaceholder":385},[288,1706,1708,1710,1713,1715,1717],{"class":155,"line":1707},50,[288,1709,1160],{"class":369},[288,1711,1712],{"class":399}," run_migrations_online",[288,1714,1166],{"class":373},[288,1716,1169],{"class":506},[288,1718,1172],{"class":373},[288,1720,1722],{"class":155,"line":1721},51,[288,1723,1724],{"class":494},"    \"\"\"Entry point called by Alembic when a live connection is available.\"\"\"\n",[288,1726,1728],{"class":155,"line":1727},52,[288,1729,1730],{"class":373},"    asyncio.run(run_async_migrations())\n",[271,1732,1734],{"id":1733},"wiring-offline-and-online-modes-together","Wiring offline and online modes together",[14,1736,1737,1738,1740],{},"The bottom of ",[18,1739,20],{}," chooses the mode based on Alembic's context flag:",[279,1742,1744],{"className":281,"code":1743,"language":283,"meta":284,"style":284},"if context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n",[18,1745,1746,1754,1759,1766],{"__ignoreMap":284},[288,1747,1748,1751],{"class":155,"line":290},[288,1749,1750],{"class":369},"if",[288,1752,1753],{"class":373}," context.is_offline_mode():\n",[288,1755,1756],{"class":155,"line":297},[288,1757,1758],{"class":373},"    run_migrations_offline()\n",[288,1760,1761,1764],{"class":155,"line":303},[288,1762,1763],{"class":369},"else",[288,1765,1172],{"class":373},[288,1767,1768],{"class":155,"line":309},[288,1769,1770],{"class":373},"    run_migrations_online()\n",[14,1772,1773,1774,1776],{},"The full ",[18,1775,20],{}," assembled:",[279,1778,1780],{"className":281,"code":1779,"language":283,"meta":284,"style":284},"# alembic\u002Fenv.py\nimport asyncio\nimport os\nfrom logging.config import fileConfig\n\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nfrom alembic import context\n\n# --- Application imports ---\nfrom app.models import Base  # pulls in all model modules via __init__.py\n\n# --- Alembic config ---\nconfig = context.config\n\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\n# Override URL from environment\nDATABASE_URL = os.environ.get(\n    \"DATABASE_URL\",\n    \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fappdb\",\n)\nconfig.set_main_option(\"sqlalchemy.url\", DATABASE_URL)\n\ntarget_metadata = Base.metadata\n\n\n# --- Offline mode ---\ndef run_migrations_offline() -> None:\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\n# --- Online mode ---\ndef do_run_migrations(connection) -> None:  # type: ignore[type-arg]\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        compare_type=True,\n        compare_server_default=True,\n        include_schemas=True,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n\n\nasync def run_async_migrations() -> None:\n    connectable = create_async_engine(\n        config.get_main_option(\"sqlalchemy.url\"),  # type: ignore[arg-type]\n        echo=False,\n        pool_pre_ping=True,\n    )\n    async with connectable.connect() as connection:\n        await connection.run_sync(do_run_migrations)\n    await connectable.dispose()\n\n\ndef run_migrations_online() -> None:\n    asyncio.run(run_async_migrations())\n\n\n# --- Dispatch ---\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n",[18,1781,1782,1787,1793,1799,1809,1813,1824,1828,1838,1842,1847,1861,1865,1870,1878,1882,1900,1905,1909,1914,1922,1928,1934,1938,1950,1954,1962,1966,1970,1975,1987,1999,2003,2011,2019,2029,2045,2055,2065,2069,2075,2079,2083,2087,2092,2109,2113,2122,2130,2140,2150,2160,2164,2171,2176,2181,2186,2201,2210,2221,2232,2243,2248,2261,2268,2275,2280,2285,2298,2303,2308,2313,2319,2326,2331,2338],{"__ignoreMap":284},[288,1783,1784],{"class":155,"line":290},[288,1785,1786],{"class":293},"# alembic\u002Fenv.py\n",[288,1788,1789,1791],{"class":155,"line":297},[288,1790,377],{"class":369},[288,1792,1327],{"class":373},[288,1794,1795,1797],{"class":155,"line":303},[288,1796,377],{"class":369},[288,1798,1002],{"class":373},[288,1800,1801,1803,1805,1807],{"class":155,"line":309},[288,1802,370],{"class":369},[288,1804,1334],{"class":373},[288,1806,377],{"class":369},[288,1808,1339],{"class":373},[288,1810,1811],{"class":155,"line":393},[288,1812,386],{"emptyLinePlaceholder":385},[288,1814,1815,1817,1819,1821],{"class":155,"line":411},[288,1816,370],{"class":369},[288,1818,1350],{"class":373},[288,1820,377],{"class":369},[288,1822,1823],{"class":373}," create_async_engine\n",[288,1825,1826],{"class":155,"line":470},[288,1827,386],{"emptyLinePlaceholder":385},[288,1829,1830,1832,1834,1836],{"class":155,"line":485},[288,1831,370],{"class":369},[288,1833,1009],{"class":373},[288,1835,377],{"class":369},[288,1837,1014],{"class":373},[288,1839,1840],{"class":155,"line":498},[288,1841,386],{"emptyLinePlaceholder":385},[288,1843,1844],{"class":155,"line":503},[288,1845,1846],{"class":293},"# --- Application imports ---\n",[288,1848,1849,1851,1853,1855,1858],{"class":155,"line":536},[288,1850,370],{"class":369},[288,1852,1381],{"class":373},[288,1854,377],{"class":369},[288,1856,1857],{"class":373}," Base  ",[288,1859,1860],{"class":293},"# pulls in all model modules via __init__.py\n",[288,1862,1863],{"class":155,"line":577},[288,1864,386],{"emptyLinePlaceholder":385},[288,1866,1867],{"class":155,"line":603},[288,1868,1869],{"class":293},"# --- Alembic config ---\n",[288,1871,1872,1874,1876],{"class":155,"line":614},[288,1873,1061],{"class":373},[288,1875,491],{"class":369},[288,1877,1066],{"class":373},[288,1879,1880],{"class":155,"line":644},[288,1881,386],{"emptyLinePlaceholder":385},[288,1883,1884,1886,1889,1892,1895,1898],{"class":155,"line":1296},[288,1885,1750],{"class":369},[288,1887,1888],{"class":373}," config.config_file_name ",[288,1890,1891],{"class":369},"is",[288,1893,1894],{"class":369}," not",[288,1896,1897],{"class":506}," None",[288,1899,1172],{"class":373},[288,1901,1902],{"class":155,"line":1305},[288,1903,1904],{"class":373},"    fileConfig(config.config_file_name)\n",[288,1906,1907],{"class":155,"line":1448},[288,1908,386],{"emptyLinePlaceholder":385},[288,1910,1911],{"class":155,"line":1454},[288,1912,1913],{"class":293},"# Override URL from environment\n",[288,1915,1916,1918,1920],{"class":155,"line":1460},[288,1917,1028],{"class":506},[288,1919,940],{"class":369},[288,1921,1033],{"class":373},[288,1923,1924,1926],{"class":155,"line":1465},[288,1925,1038],{"class":494},[288,1927,1041],{"class":373},[288,1929,1930,1932],{"class":155,"line":1470},[288,1931,1046],{"class":494},[288,1933,1041],{"class":373},[288,1935,1936],{"class":155,"line":1483},[288,1937,533],{"class":373},[288,1939,1940,1942,1944,1946,1948],{"class":155,"line":1492},[288,1941,1071],{"class":373},[288,1943,1074],{"class":494},[288,1945,564],{"class":373},[288,1947,1028],{"class":506},[288,1949,533],{"class":373},[288,1951,1952],{"class":155,"line":1503},[288,1953,386],{"emptyLinePlaceholder":385},[288,1955,1956,1958,1960],{"class":155,"line":1514},[288,1957,1394],{"class":373},[288,1959,491],{"class":369},[288,1961,1399],{"class":373},[288,1963,1964],{"class":155,"line":1526},[288,1965,386],{"emptyLinePlaceholder":385},[288,1967,1968],{"class":155,"line":1531},[288,1969,386],{"emptyLinePlaceholder":385},[288,1971,1972],{"class":155,"line":1536},[288,1973,1974],{"class":293},"# --- Offline mode ---\n",[288,1976,1977,1979,1981,1983,1985],{"class":155,"line":1543},[288,1978,1160],{"class":369},[288,1980,1163],{"class":399},[288,1982,1166],{"class":373},[288,1984,1169],{"class":506},[288,1986,1172],{"class":373},[288,1988,1989,1991,1993,1995,1997],{"class":155,"line":1548},[288,1990,1196],{"class":373},[288,1992,491],{"class":369},[288,1994,1201],{"class":373},[288,1996,1074],{"class":494},[288,1998,533],{"class":373},[288,2000,2001],{"class":155,"line":1553},[288,2002,1210],{"class":373},[288,2004,2005,2007,2009],{"class":155,"line":1558},[288,2006,1215],{"class":524},[288,2008,491],{"class":369},[288,2010,1220],{"class":373},[288,2012,2013,2015,2017],{"class":155,"line":1576},[288,2014,1225],{"class":524},[288,2016,491],{"class":369},[288,2018,1230],{"class":373},[288,2020,2021,2023,2025,2027],{"class":155,"line":1581},[288,2022,1235],{"class":524},[288,2024,491],{"class":369},[288,2026,530],{"class":506},[288,2028,1041],{"class":373},[288,2030,2031,2033,2035,2037,2039,2041,2043],{"class":155,"line":1587},[288,2032,1246],{"class":524},[288,2034,491],{"class":369},[288,2036,1251],{"class":373},[288,2038,1254],{"class":494},[288,2040,1257],{"class":373},[288,2042,1260],{"class":494},[288,2044,1263],{"class":373},[288,2046,2047,2049,2051,2053],{"class":155,"line":1593},[288,2048,1268],{"class":524},[288,2050,491],{"class":369},[288,2052,530],{"class":506},[288,2054,1041],{"class":373},[288,2056,2057,2059,2061,2063],{"class":155,"line":1598},[288,2058,1279],{"class":524},[288,2060,491],{"class":369},[288,2062,530],{"class":506},[288,2064,1041],{"class":373},[288,2066,2067],{"class":155,"line":1609},[288,2068,647],{"class":373},[288,2070,2071,2073],{"class":155,"line":1622},[288,2072,1299],{"class":369},[288,2074,1302],{"class":373},[288,2076,2077],{"class":155,"line":1634},[288,2078,1308],{"class":373},[288,2080,2081],{"class":155,"line":1646},[288,2082,386],{"emptyLinePlaceholder":385},[288,2084,2085],{"class":155,"line":1651},[288,2086,386],{"emptyLinePlaceholder":385},[288,2088,2089],{"class":155,"line":1656},[288,2090,2091],{"class":293},"# --- Online mode ---\n",[288,2093,2094,2096,2098,2101,2103,2106],{"class":155,"line":1674},[288,2095,1160],{"class":369},[288,2097,1434],{"class":399},[288,2099,2100],{"class":373},"(connection) -> ",[288,2102,1169],{"class":506},[288,2104,2105],{"class":373},":  ",[288,2107,2108],{"class":293},"# type: ignore[type-arg]\n",[288,2110,2111],{"class":155,"line":1683},[288,2112,1210],{"class":373},[288,2114,2115,2117,2119],{"class":155,"line":1688},[288,2116,1473],{"class":524},[288,2118,491],{"class":369},[288,2120,2121],{"class":373},"connection,\n",[288,2123,2124,2126,2128],{"class":155,"line":1697},[288,2125,1225],{"class":524},[288,2127,491],{"class":369},[288,2129,1230],{"class":373},[288,2131,2132,2134,2136,2138],{"class":155,"line":1702},[288,2133,1268],{"class":524},[288,2135,491],{"class":369},[288,2137,530],{"class":506},[288,2139,1041],{"class":373},[288,2141,2142,2144,2146,2148],{"class":155,"line":1707},[288,2143,1279],{"class":524},[288,2145,491],{"class":369},[288,2147,530],{"class":506},[288,2149,1041],{"class":373},[288,2151,2152,2154,2156,2158],{"class":155,"line":1721},[288,2153,1517],{"class":524},[288,2155,491],{"class":369},[288,2157,530],{"class":506},[288,2159,1041],{"class":373},[288,2161,2162],{"class":155,"line":1727},[288,2163,647],{"class":373},[288,2165,2167,2169],{"class":155,"line":2166},53,[288,2168,1299],{"class":369},[288,2170,1302],{"class":373},[288,2172,2174],{"class":155,"line":2173},54,[288,2175,1308],{"class":373},[288,2177,2179],{"class":155,"line":2178},55,[288,2180,386],{"emptyLinePlaceholder":385},[288,2182,2184],{"class":155,"line":2183},56,[288,2185,386],{"emptyLinePlaceholder":385},[288,2187,2189,2191,2193,2195,2197,2199],{"class":155,"line":2188},57,[288,2190,1561],{"class":369},[288,2192,1564],{"class":369},[288,2194,1567],{"class":399},[288,2196,1166],{"class":373},[288,2198,1169],{"class":506},[288,2200,1172],{"class":373},[288,2202,2204,2206,2208],{"class":155,"line":2203},58,[288,2205,1601],{"class":373},[288,2207,491],{"class":369},[288,2209,1606],{"class":373},[288,2211,2213,2215,2217,2219],{"class":155,"line":2212},59,[288,2214,1612],{"class":373},[288,2216,1074],{"class":494},[288,2218,1617],{"class":373},[288,2220,1419],{"class":293},[288,2222,2224,2226,2228,2230],{"class":155,"line":2223},60,[288,2225,1625],{"class":524},[288,2227,491],{"class":369},[288,2229,572],{"class":506},[288,2231,1041],{"class":373},[288,2233,2235,2237,2239,2241],{"class":155,"line":2234},61,[288,2236,1637],{"class":524},[288,2238,491],{"class":369},[288,2240,530],{"class":506},[288,2242,1041],{"class":373},[288,2244,2246],{"class":155,"line":2245},62,[288,2247,647],{"class":373},[288,2249,2251,2253,2255,2257,2259],{"class":155,"line":2250},63,[288,2252,1659],{"class":369},[288,2254,1662],{"class":369},[288,2256,1665],{"class":373},[288,2258,1668],{"class":369},[288,2260,1671],{"class":373},[288,2262,2264,2266],{"class":155,"line":2263},64,[288,2265,1677],{"class":369},[288,2267,1680],{"class":373},[288,2269,2271,2273],{"class":155,"line":2270},65,[288,2272,1691],{"class":369},[288,2274,1694],{"class":373},[288,2276,2278],{"class":155,"line":2277},66,[288,2279,386],{"emptyLinePlaceholder":385},[288,2281,2283],{"class":155,"line":2282},67,[288,2284,386],{"emptyLinePlaceholder":385},[288,2286,2288,2290,2292,2294,2296],{"class":155,"line":2287},68,[288,2289,1160],{"class":369},[288,2291,1712],{"class":399},[288,2293,1166],{"class":373},[288,2295,1169],{"class":506},[288,2297,1172],{"class":373},[288,2299,2301],{"class":155,"line":2300},69,[288,2302,1730],{"class":373},[288,2304,2306],{"class":155,"line":2305},70,[288,2307,386],{"emptyLinePlaceholder":385},[288,2309,2311],{"class":155,"line":2310},71,[288,2312,386],{"emptyLinePlaceholder":385},[288,2314,2316],{"class":155,"line":2315},72,[288,2317,2318],{"class":293},"# --- Dispatch ---\n",[288,2320,2322,2324],{"class":155,"line":2321},73,[288,2323,1750],{"class":369},[288,2325,1753],{"class":373},[288,2327,2329],{"class":155,"line":2328},74,[288,2330,1758],{"class":373},[288,2332,2334,2336],{"class":155,"line":2333},75,[288,2335,1763],{"class":369},[288,2337,1172],{"class":373},[288,2339,2341],{"class":155,"line":2340},76,[288,2342,1770],{"class":373},[14,2344,2345,2346,2348,2349,2351,2352,2356],{},"For the detailed step-by-step walkthrough of each ",[18,2347,20],{}," configuration option and the full ",[18,2350,322],{}," setup, see ",[27,2353,2355],{"href":2354},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002Fsetting-up-alembic-env-py-for-asyncpg\u002F","Setting Up Alembic env.py for asyncpg",".",[33,2358,2360],{"id":2359},"state-management-and-session-boundaries","State Management and Session Boundaries",[271,2362,2364],{"id":2363},"why-migrations-use-a-separate-engine","Why migrations use a separate engine",[14,2366,2367,2368,2370,2371,2374,2375,2378,2379,2356],{},"The engine in ",[18,2369,20],{}," must not be the same object your application uses for request handling. The application engine is typically created at startup and persists for the process lifetime, maintaining an open connection pool. The migration engine is ephemeral: it is created inside ",[18,2372,2373],{},"run_async_migrations()",", used for the duration of the ",[18,2376,2377],{},"alembic upgrade head"," command, and disposed immediately after via ",[18,2380,2381],{},"await connectable.dispose()",[14,2383,2384],{},"Mixing the two creates problems:",[2386,2387,2388,2391,2397],"ul",{},[55,2389,2390],{},"The application pool may have open transactions that conflict with DDL locks.",[55,2392,2393,2394,2396],{},"Pool connections are bound to the event loop that created them. The migration script's ",[18,2395,60],{}," creates a new event loop; connections from the application's engine are invalid in it.",[55,2398,2399,2402],{},[18,2400,2401],{},"pool_pre_ping=True"," on the migration engine catches stale connections that may have accumulated if the database was restarted between CI steps.",[271,2404,2406],{"id":2405},"transaction-management-in-migrations","Transaction management in migrations",[14,2408,2409,2410,2413,2414,564,2417,2420,2421,2424,2425,2428],{},"Alembic wraps each migration script in a transaction by default (",[18,2411,2412],{},"with context.begin_transaction()","). PostgreSQL supports transactional DDL, meaning ",[18,2415,2416],{},"CREATE TABLE",[18,2418,2419],{},"ALTER TABLE",", and ",[18,2422,2423],{},"CREATE INDEX"," can be rolled back if the migration fails mid-way. This is a significant safety guarantee. Do not disable transaction wrapping unless you are using a DDL statement that PostgreSQL does not allow inside a transaction, such as ",[18,2426,2427],{},"CREATE INDEX CONCURRENTLY",". In that case, mark the migration as non-transactional:",[279,2430,2432],{"className":281,"code":2431,"language":283,"meta":284,"style":284},"# versions\u002F20240618_add_invoice_index.py\nfrom alembic import op\n\n# directive to opt out of the wrapping transaction for this migration\ndef upgrade() -> None:\n    op.execute(\"COMMIT\")  # close the transaction alembic opened\n    op.create_index(\n        \"ix_invoices_tenant_id\",\n        \"invoices\",\n        [\"tenant_id\"],\n        postgresql_concurrently=True,\n    )\n",[18,2433,2434,2439,2450,2454,2459,2472,2486,2491,2498,2505,2516,2527],{"__ignoreMap":284},[288,2435,2436],{"class":155,"line":290},[288,2437,2438],{"class":293},"# versions\u002F20240618_add_invoice_index.py\n",[288,2440,2441,2443,2445,2447],{"class":155,"line":297},[288,2442,370],{"class":369},[288,2444,1009],{"class":373},[288,2446,377],{"class":369},[288,2448,2449],{"class":373}," op\n",[288,2451,2452],{"class":155,"line":303},[288,2453,386],{"emptyLinePlaceholder":385},[288,2455,2456],{"class":155,"line":309},[288,2457,2458],{"class":293},"# directive to opt out of the wrapping transaction for this migration\n",[288,2460,2461,2463,2466,2468,2470],{"class":155,"line":393},[288,2462,1160],{"class":369},[288,2464,2465],{"class":399}," upgrade",[288,2467,1166],{"class":373},[288,2469,1169],{"class":506},[288,2471,1172],{"class":373},[288,2473,2474,2477,2480,2483],{"class":155,"line":411},[288,2475,2476],{"class":373},"    op.execute(",[288,2478,2479],{"class":494},"\"COMMIT\"",[288,2481,2482],{"class":373},")  ",[288,2484,2485],{"class":293},"# close the transaction alembic opened\n",[288,2487,2488],{"class":155,"line":470},[288,2489,2490],{"class":373},"    op.create_index(\n",[288,2492,2493,2496],{"class":155,"line":485},[288,2494,2495],{"class":494},"        \"ix_invoices_tenant_id\"",[288,2497,1041],{"class":373},[288,2499,2500,2503],{"class":155,"line":498},[288,2501,2502],{"class":494},"        \"invoices\"",[288,2504,1041],{"class":373},[288,2506,2507,2510,2513],{"class":155,"line":503},[288,2508,2509],{"class":373},"        [",[288,2511,2512],{"class":494},"\"tenant_id\"",[288,2514,2515],{"class":373},"],\n",[288,2517,2518,2521,2523,2525],{"class":155,"line":536},[288,2519,2520],{"class":524},"        postgresql_concurrently",[288,2522,491],{"class":369},[288,2524,530],{"class":506},[288,2526,1041],{"class":373},[288,2528,2529],{"class":155,"line":577},[288,2530,647],{"class":373},[14,2532,2533,2534,2356],{},"For zero-downtime DDL strategies like concurrent index creation, see ",[27,2535,2537],{"href":2536},"\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002F","Zero-Downtime Schema Migration Strategies",[271,2539,2541],{"id":2540},"sharing-metadata-across-application-and-migration-layers","Sharing metadata across application and migration layers",[14,2543,2544,2547,2548,2550,2551,853,2553,2555],{},[18,2545,2546],{},"target_metadata = Base.metadata"," creates a reference, not a copy. Any change to ",[18,2549,350],{}," after this line — such as dynamically registering new tables — is reflected in what Alembic compares against the live schema. Ensure all model imports complete before ",[18,2552,20],{},[18,2554,343],{},". A delayed import or a lazy model registration (common in plugin architectures) will cause autogenerate to miss tables silently.",[14,2557,2558,2559,2561],{},"The safest pattern for large applications with many model modules is to enumerate imports explicitly in ",[18,2560,859],{}," rather than relying on autodiscovery. This makes the import surface visible and auditable.",[33,2563,2565],{"id":2564},"advanced-configuration-patterns","Advanced Configuration Patterns",[271,2567,2569],{"id":2568},"multi-schema-deployments","Multi-schema deployments",[14,2571,2572,2573,2576,2577,2580,2581,344,2584,2586,2587,2590],{},"Applications that split models across PostgreSQL schemas (e.g., a ",[18,2574,2575],{},"public"," schema for shared tables and a ",[18,2578,2579],{},"tenants"," schema for per-tenant tables) need ",[18,2582,2583],{},"include_schemas=True",[18,2585,204],{}," and an ",[18,2588,2589],{},"include_object"," filter:",[279,2592,2594],{"className":281,"code":2593,"language":283,"meta":284,"style":284},"from alembic.runtime.migration import MigrationContext\nfrom alembic.operations import Operations\n\n\ndef include_object(object, name, type_, reflected, compare_to):\n    # Only autogenerate for schemas we own\n    if type_ == \"table\":\n        return object.schema in (None, \"public\", \"tenants\")\n    return True\n\n\ndef do_run_migrations(connection) -> None:  # type: ignore[type-arg]\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        compare_type=True,\n        compare_server_default=True,\n        include_schemas=True,\n        include_object=include_object,\n        version_table_schema=\"public\",  # keep alembic_version in public schema\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n",[18,2595,2596,2608,2620,2624,2628,2638,2643,2659,2690,2698,2702,2706,2720,2724,2732,2740,2750,2760,2770,2780,2795,2799,2805],{"__ignoreMap":284},[288,2597,2598,2600,2603,2605],{"class":155,"line":290},[288,2599,370],{"class":369},[288,2601,2602],{"class":373}," alembic.runtime.migration ",[288,2604,377],{"class":369},[288,2606,2607],{"class":373}," MigrationContext\n",[288,2609,2610,2612,2615,2617],{"class":155,"line":297},[288,2611,370],{"class":369},[288,2613,2614],{"class":373}," alembic.operations ",[288,2616,377],{"class":369},[288,2618,2619],{"class":373}," Operations\n",[288,2621,2622],{"class":155,"line":303},[288,2623,386],{"emptyLinePlaceholder":385},[288,2625,2626],{"class":155,"line":309},[288,2627,386],{"emptyLinePlaceholder":385},[288,2629,2630,2632,2635],{"class":155,"line":393},[288,2631,1160],{"class":369},[288,2633,2634],{"class":399}," include_object",[288,2636,2637],{"class":373},"(object, name, type_, reflected, compare_to):\n",[288,2639,2640],{"class":155,"line":411},[288,2641,2642],{"class":293},"    # Only autogenerate for schemas we own\n",[288,2644,2645,2648,2651,2654,2657],{"class":155,"line":470},[288,2646,2647],{"class":369},"    if",[288,2649,2650],{"class":373}," type_ ",[288,2652,2653],{"class":369},"==",[288,2655,2656],{"class":494}," \"table\"",[288,2658,1172],{"class":373},[288,2660,2661,2664,2667,2670,2673,2676,2678,2680,2683,2685,2688],{"class":155,"line":485},[288,2662,2663],{"class":369},"        return",[288,2665,2666],{"class":506}," object",[288,2668,2669],{"class":373},".schema ",[288,2671,2672],{"class":369},"in",[288,2674,2675],{"class":373}," (",[288,2677,1169],{"class":506},[288,2679,564],{"class":373},[288,2681,2682],{"class":494},"\"public\"",[288,2684,564],{"class":373},[288,2686,2687],{"class":494},"\"tenants\"",[288,2689,533],{"class":373},[288,2691,2692,2695],{"class":155,"line":498},[288,2693,2694],{"class":369},"    return",[288,2696,2697],{"class":506}," True\n",[288,2699,2700],{"class":155,"line":503},[288,2701,386],{"emptyLinePlaceholder":385},[288,2703,2704],{"class":155,"line":536},[288,2705,386],{"emptyLinePlaceholder":385},[288,2707,2708,2710,2712,2714,2716,2718],{"class":155,"line":577},[288,2709,1160],{"class":369},[288,2711,1434],{"class":399},[288,2713,2100],{"class":373},[288,2715,1169],{"class":506},[288,2717,2105],{"class":373},[288,2719,2108],{"class":293},[288,2721,2722],{"class":155,"line":603},[288,2723,1210],{"class":373},[288,2725,2726,2728,2730],{"class":155,"line":614},[288,2727,1473],{"class":524},[288,2729,491],{"class":369},[288,2731,2121],{"class":373},[288,2733,2734,2736,2738],{"class":155,"line":644},[288,2735,1225],{"class":524},[288,2737,491],{"class":369},[288,2739,1230],{"class":373},[288,2741,2742,2744,2746,2748],{"class":155,"line":1296},[288,2743,1268],{"class":524},[288,2745,491],{"class":369},[288,2747,530],{"class":506},[288,2749,1041],{"class":373},[288,2751,2752,2754,2756,2758],{"class":155,"line":1305},[288,2753,1279],{"class":524},[288,2755,491],{"class":369},[288,2757,530],{"class":506},[288,2759,1041],{"class":373},[288,2761,2762,2764,2766,2768],{"class":155,"line":1448},[288,2763,1517],{"class":524},[288,2765,491],{"class":369},[288,2767,530],{"class":506},[288,2769,1041],{"class":373},[288,2771,2772,2775,2777],{"class":155,"line":1454},[288,2773,2774],{"class":524},"        include_object",[288,2776,491],{"class":369},[288,2778,2779],{"class":373},"include_object,\n",[288,2781,2782,2785,2787,2789,2792],{"class":155,"line":1460},[288,2783,2784],{"class":524},"        version_table_schema",[288,2786,491],{"class":369},[288,2788,2682],{"class":494},[288,2790,2791],{"class":373},",  ",[288,2793,2794],{"class":293},"# keep alembic_version in public schema\n",[288,2796,2797],{"class":155,"line":1465},[288,2798,647],{"class":373},[288,2800,2801,2803],{"class":155,"line":1470},[288,2802,1299],{"class":369},[288,2804,1302],{"class":373},[288,2806,2807],{"class":155,"line":1483},[288,2808,1308],{"class":373},[271,2810,2812],{"id":2811},"tenant-aware-migrations","Tenant-aware migrations",[14,2814,2815,2816,2818],{},"SaaS platforms often need to apply the same migration to multiple tenant schemas. The pattern creates an async engine once, then loops over schemas within a single ",[18,2817,24],{}," call:",[279,2820,2822],{"className":281,"code":2821,"language":283,"meta":284,"style":284},"import asyncio\nfrom sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import create_async_engine\nfrom alembic import context\nfrom app.models import Base\nfrom app.db import get_all_tenant_schemas  # returns List[str]\n\ntarget_metadata = Base.metadata\n\n\nasync def run_tenant_migrations() -> None:\n    tenant_schemas = await get_all_tenant_schemas()\n\n    connectable = create_async_engine(\n        context.config.get_main_option(\"sqlalchemy.url\"),  # type: ignore[arg-type]\n        echo=False,\n        pool_pre_ping=True,\n    )\n\n    for schema in tenant_schemas:\n        async with connectable.connect() as connection:\n            await connection.execute(\n                text(f\"SET search_path TO {schema}\")\n            )\n            await connection.run_sync(\n                lambda conn: _configure_and_run(conn, schema)\n            )\n\n    await connectable.dispose()\n\n\ndef _configure_and_run(connection, schema: str) -> None:\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        compare_type=True,\n        version_table=f\"alembic_version_{schema}\",\n        include_schemas=False,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n",[18,2823,2824,2830,2841,2851,2861,2871,2886,2890,2898,2902,2906,2921,2934,2938,2946,2957,2967,2977,2981,2985,2998,3011,3019,3043,3048,3055,3063,3067,3071,3077,3081,3085,3104,3108,3116,3124,3134,3156,3166,3170,3176],{"__ignoreMap":284},[288,2825,2826,2828],{"class":155,"line":290},[288,2827,377],{"class":369},[288,2829,1327],{"class":373},[288,2831,2832,2834,2836,2838],{"class":155,"line":297},[288,2833,370],{"class":369},[288,2835,431],{"class":373},[288,2837,377],{"class":369},[288,2839,2840],{"class":373}," text\n",[288,2842,2843,2845,2847,2849],{"class":155,"line":303},[288,2844,370],{"class":369},[288,2846,1350],{"class":373},[288,2848,377],{"class":369},[288,2850,1823],{"class":373},[288,2852,2853,2855,2857,2859],{"class":155,"line":309},[288,2854,370],{"class":369},[288,2856,1009],{"class":373},[288,2858,377],{"class":369},[288,2860,1014],{"class":373},[288,2862,2863,2865,2867,2869],{"class":155,"line":393},[288,2864,370],{"class":369},[288,2866,1381],{"class":373},[288,2868,377],{"class":369},[288,2870,459],{"class":373},[288,2872,2873,2875,2878,2880,2883],{"class":155,"line":411},[288,2874,370],{"class":369},[288,2876,2877],{"class":373}," app.db ",[288,2879,377],{"class":369},[288,2881,2882],{"class":373}," get_all_tenant_schemas  ",[288,2884,2885],{"class":293},"# returns List[str]\n",[288,2887,2888],{"class":155,"line":470},[288,2889,386],{"emptyLinePlaceholder":385},[288,2891,2892,2894,2896],{"class":155,"line":485},[288,2893,1394],{"class":373},[288,2895,491],{"class":369},[288,2897,1399],{"class":373},[288,2899,2900],{"class":155,"line":498},[288,2901,386],{"emptyLinePlaceholder":385},[288,2903,2904],{"class":155,"line":503},[288,2905,386],{"emptyLinePlaceholder":385},[288,2907,2908,2910,2912,2915,2917,2919],{"class":155,"line":536},[288,2909,1561],{"class":369},[288,2911,1564],{"class":369},[288,2913,2914],{"class":399}," run_tenant_migrations",[288,2916,1166],{"class":373},[288,2918,1169],{"class":506},[288,2920,1172],{"class":373},[288,2922,2923,2926,2928,2931],{"class":155,"line":577},[288,2924,2925],{"class":373},"    tenant_schemas ",[288,2927,491],{"class":369},[288,2929,2930],{"class":369}," await",[288,2932,2933],{"class":373}," get_all_tenant_schemas()\n",[288,2935,2936],{"class":155,"line":603},[288,2937,386],{"emptyLinePlaceholder":385},[288,2939,2940,2942,2944],{"class":155,"line":614},[288,2941,1601],{"class":373},[288,2943,491],{"class":369},[288,2945,1606],{"class":373},[288,2947,2948,2951,2953,2955],{"class":155,"line":644},[288,2949,2950],{"class":373},"        context.config.get_main_option(",[288,2952,1074],{"class":494},[288,2954,1617],{"class":373},[288,2956,1419],{"class":293},[288,2958,2959,2961,2963,2965],{"class":155,"line":1296},[288,2960,1625],{"class":524},[288,2962,491],{"class":369},[288,2964,572],{"class":506},[288,2966,1041],{"class":373},[288,2968,2969,2971,2973,2975],{"class":155,"line":1305},[288,2970,1637],{"class":524},[288,2972,491],{"class":369},[288,2974,530],{"class":506},[288,2976,1041],{"class":373},[288,2978,2979],{"class":155,"line":1448},[288,2980,647],{"class":373},[288,2982,2983],{"class":155,"line":1454},[288,2984,386],{"emptyLinePlaceholder":385},[288,2986,2987,2990,2993,2995],{"class":155,"line":1460},[288,2988,2989],{"class":369},"    for",[288,2991,2992],{"class":373}," schema ",[288,2994,2672],{"class":369},[288,2996,2997],{"class":373}," tenant_schemas:\n",[288,2999,3000,3003,3005,3007,3009],{"class":155,"line":1465},[288,3001,3002],{"class":369},"        async",[288,3004,1662],{"class":369},[288,3006,1665],{"class":373},[288,3008,1668],{"class":369},[288,3010,1671],{"class":373},[288,3012,3013,3016],{"class":155,"line":1470},[288,3014,3015],{"class":369},"            await",[288,3017,3018],{"class":373}," connection.execute(\n",[288,3020,3021,3024,3027,3030,3032,3035,3038,3041],{"class":155,"line":1483},[288,3022,3023],{"class":373},"                text(",[288,3025,3026],{"class":369},"f",[288,3028,3029],{"class":494},"\"SET search_path TO ",[288,3031,1251],{"class":506},[288,3033,3034],{"class":373},"schema",[288,3036,3037],{"class":506},"}",[288,3039,3040],{"class":494},"\"",[288,3042,533],{"class":373},[288,3044,3045],{"class":155,"line":1492},[288,3046,3047],{"class":373},"            )\n",[288,3049,3050,3052],{"class":155,"line":1503},[288,3051,3015],{"class":369},[288,3053,3054],{"class":373}," connection.run_sync(\n",[288,3056,3057,3060],{"class":155,"line":1514},[288,3058,3059],{"class":369},"                lambda",[288,3061,3062],{"class":373}," conn: _configure_and_run(conn, schema)\n",[288,3064,3065],{"class":155,"line":1526},[288,3066,3047],{"class":373},[288,3068,3069],{"class":155,"line":1531},[288,3070,386],{"emptyLinePlaceholder":385},[288,3072,3073,3075],{"class":155,"line":1536},[288,3074,1691],{"class":369},[288,3076,1694],{"class":373},[288,3078,3079],{"class":155,"line":1543},[288,3080,386],{"emptyLinePlaceholder":385},[288,3082,3083],{"class":155,"line":1548},[288,3084,386],{"emptyLinePlaceholder":385},[288,3086,3087,3089,3092,3095,3097,3100,3102],{"class":155,"line":1553},[288,3088,1160],{"class":369},[288,3090,3091],{"class":399}," _configure_and_run",[288,3093,3094],{"class":373},"(connection, schema: ",[288,3096,542],{"class":506},[288,3098,3099],{"class":373},") -> ",[288,3101,1169],{"class":506},[288,3103,1172],{"class":373},[288,3105,3106],{"class":155,"line":1558},[288,3107,1210],{"class":373},[288,3109,3110,3112,3114],{"class":155,"line":1576},[288,3111,1473],{"class":524},[288,3113,491],{"class":369},[288,3115,2121],{"class":373},[288,3117,3118,3120,3122],{"class":155,"line":1581},[288,3119,1225],{"class":524},[288,3121,491],{"class":369},[288,3123,1230],{"class":373},[288,3125,3126,3128,3130,3132],{"class":155,"line":1587},[288,3127,1268],{"class":524},[288,3129,491],{"class":369},[288,3131,530],{"class":506},[288,3133,1041],{"class":373},[288,3135,3136,3139,3141,3143,3146,3148,3150,3152,3154],{"class":155,"line":1593},[288,3137,3138],{"class":524},"        version_table",[288,3140,491],{"class":369},[288,3142,3026],{"class":369},[288,3144,3145],{"class":494},"\"alembic_version_",[288,3147,1251],{"class":506},[288,3149,3034],{"class":373},[288,3151,3037],{"class":506},[288,3153,3040],{"class":494},[288,3155,1041],{"class":373},[288,3157,3158,3160,3162,3164],{"class":155,"line":1598},[288,3159,1517],{"class":524},[288,3161,491],{"class":369},[288,3163,572],{"class":506},[288,3165,1041],{"class":373},[288,3167,3168],{"class":155,"line":1609},[288,3169,647],{"class":373},[288,3171,3172,3174],{"class":155,"line":1622},[288,3173,1299],{"class":369},[288,3175,1302],{"class":373},[288,3177,3178],{"class":155,"line":1634},[288,3179,1308],{"class":373},[14,3181,3182,3183,3186],{},"This approach keeps one connection pool for the migration run while touching each ",[18,3184,3185],{},"Tenant"," schema sequentially. Parallel schema migration is possible but risks DDL lock contention on shared catalog tables.",[271,3188,3190],{"id":3189},"programmatic-migration-invocation-from-fastapi-startup","Programmatic migration invocation from FastAPI startup",[14,3192,3193,3194,3196],{},"Some deployment patterns run ",[18,3195,2377],{}," programmatically during application startup rather than as a separate CLI step. This is common in Kubernetes init-container patterns and Docker Compose setups:",[279,3198,3200],{"className":281,"code":3199,"language":283,"meta":284,"style":284},"# app\u002Fdb\u002Fmigrations.py\nimport asyncio\nfrom alembic.config import Config\nfrom alembic import command\n\n\ndef run_migrations_sync() -> None:\n    \"\"\"\n    Invoke alembic upgrade head programmatically.\n    Must be called before the application event loop starts\n    or from a dedicated thread, never from within a running loop.\n    \"\"\"\n    alembic_cfg = Config(\"alembic.ini\")\n    command.upgrade(alembic_cfg, \"head\")\n\n\n# FastAPI lifespan — run migrations before accepting traffic\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    # run_in_executor keeps the synchronous alembic call off the event loop\n    loop = asyncio.get_event_loop()\n    await loop.run_in_executor(None, run_migrations_sync)\n    yield\n    # shutdown cleanup here\n\n\napp = FastAPI(lifespan=lifespan)\n",[18,3201,3202,3207,3213,3225,3236,3240,3244,3257,3261,3266,3271,3276,3280,3295,3305,3309,3313,3318,3330,3342,3346,3350,3355,3367,3372,3382,3394,3399,3404,3408,3412],{"__ignoreMap":284},[288,3203,3204],{"class":155,"line":290},[288,3205,3206],{"class":293},"# app\u002Fdb\u002Fmigrations.py\n",[288,3208,3209,3211],{"class":155,"line":297},[288,3210,377],{"class":369},[288,3212,1327],{"class":373},[288,3214,3215,3217,3220,3222],{"class":155,"line":303},[288,3216,370],{"class":369},[288,3218,3219],{"class":373}," alembic.config ",[288,3221,377],{"class":369},[288,3223,3224],{"class":373}," Config\n",[288,3226,3227,3229,3231,3233],{"class":155,"line":309},[288,3228,370],{"class":369},[288,3230,1009],{"class":373},[288,3232,377],{"class":369},[288,3234,3235],{"class":373}," command\n",[288,3237,3238],{"class":155,"line":393},[288,3239,386],{"emptyLinePlaceholder":385},[288,3241,3242],{"class":155,"line":411},[288,3243,386],{"emptyLinePlaceholder":385},[288,3245,3246,3248,3251,3253,3255],{"class":155,"line":470},[288,3247,1160],{"class":369},[288,3249,3250],{"class":399}," run_migrations_sync",[288,3252,1166],{"class":373},[288,3254,1169],{"class":506},[288,3256,1172],{"class":373},[288,3258,3259],{"class":155,"line":485},[288,3260,1177],{"class":494},[288,3262,3263],{"class":155,"line":498},[288,3264,3265],{"class":494},"    Invoke alembic upgrade head programmatically.\n",[288,3267,3268],{"class":155,"line":503},[288,3269,3270],{"class":494},"    Must be called before the application event loop starts\n",[288,3272,3273],{"class":155,"line":536},[288,3274,3275],{"class":494},"    or from a dedicated thread, never from within a running loop.\n",[288,3277,3278],{"class":155,"line":577},[288,3279,1177],{"class":494},[288,3281,3282,3285,3287,3290,3293],{"class":155,"line":603},[288,3283,3284],{"class":373},"    alembic_cfg ",[288,3286,491],{"class":369},[288,3288,3289],{"class":373}," Config(",[288,3291,3292],{"class":494},"\"alembic.ini\"",[288,3294,533],{"class":373},[288,3296,3297,3300,3303],{"class":155,"line":614},[288,3298,3299],{"class":373},"    command.upgrade(alembic_cfg, ",[288,3301,3302],{"class":494},"\"head\"",[288,3304,533],{"class":373},[288,3306,3307],{"class":155,"line":644},[288,3308,386],{"emptyLinePlaceholder":385},[288,3310,3311],{"class":155,"line":1296},[288,3312,386],{"emptyLinePlaceholder":385},[288,3314,3315],{"class":155,"line":1305},[288,3316,3317],{"class":293},"# FastAPI lifespan — run migrations before accepting traffic\n",[288,3319,3320,3322,3325,3327],{"class":155,"line":1448},[288,3321,370],{"class":369},[288,3323,3324],{"class":373}," contextlib ",[288,3326,377],{"class":369},[288,3328,3329],{"class":373}," asynccontextmanager\n",[288,3331,3332,3334,3337,3339],{"class":155,"line":1454},[288,3333,370],{"class":369},[288,3335,3336],{"class":373}," fastapi ",[288,3338,377],{"class":369},[288,3340,3341],{"class":373}," FastAPI\n",[288,3343,3344],{"class":155,"line":1460},[288,3345,386],{"emptyLinePlaceholder":385},[288,3347,3348],{"class":155,"line":1465},[288,3349,386],{"emptyLinePlaceholder":385},[288,3351,3352],{"class":155,"line":1470},[288,3353,3354],{"class":399},"@asynccontextmanager\n",[288,3356,3357,3359,3361,3364],{"class":155,"line":1483},[288,3358,1561],{"class":369},[288,3360,1564],{"class":369},[288,3362,3363],{"class":399}," lifespan",[288,3365,3366],{"class":373},"(app: FastAPI):\n",[288,3368,3369],{"class":155,"line":1492},[288,3370,3371],{"class":293},"    # run_in_executor keeps the synchronous alembic call off the event loop\n",[288,3373,3374,3377,3379],{"class":155,"line":1503},[288,3375,3376],{"class":373},"    loop ",[288,3378,491],{"class":369},[288,3380,3381],{"class":373}," asyncio.get_event_loop()\n",[288,3383,3384,3386,3389,3391],{"class":155,"line":1514},[288,3385,1691],{"class":369},[288,3387,3388],{"class":373}," loop.run_in_executor(",[288,3390,1169],{"class":506},[288,3392,3393],{"class":373},", run_migrations_sync)\n",[288,3395,3396],{"class":155,"line":1526},[288,3397,3398],{"class":369},"    yield\n",[288,3400,3401],{"class":155,"line":1531},[288,3402,3403],{"class":293},"    # shutdown cleanup here\n",[288,3405,3406],{"class":155,"line":1536},[288,3407,386],{"emptyLinePlaceholder":385},[288,3409,3410],{"class":155,"line":1543},[288,3411,386],{"emptyLinePlaceholder":385},[288,3413,3414,3417,3419,3422,3425,3427],{"class":155,"line":1548},[288,3415,3416],{"class":373},"app ",[288,3418,491],{"class":369},[288,3420,3421],{"class":373}," FastAPI(",[288,3423,3424],{"class":524},"lifespan",[288,3426,491],{"class":369},[288,3428,3429],{"class":373},"lifespan)\n",[14,3431,3432,3433,3436,3437,3439,3440,3442,3443,3446,3447,3450],{},"Note that ",[18,3434,3435],{},"command.upgrade"," is synchronous and calls ",[18,3438,60],{}," internally (through ",[18,3441,20],{},"). Invoking it from inside a running event loop causes ",[18,3444,3445],{},"RuntimeError: This event loop is already running",". The ",[18,3448,3449],{},"run_in_executor"," pattern sidesteps this by running the synchronous Alembic call in a thread pool where no event loop is active.",[33,3452,3454],{"id":3453},"comparison-configuration-approaches","Comparison: Configuration Approaches",[3456,3457,3458,3480],"table",{},[3459,3460,3461],"thead",{},[3462,3463,3464,3468,3471,3474,3477],"tr",{},[3465,3466,3467],"th",{},"Approach",[3465,3469,3470],{},"URL Source",[3465,3472,3473],{},"Env Isolation",[3465,3475,3476],{},"CI-Friendly",[3465,3478,3479],{},"Multi-Tenant",[3481,3482,3483,3501,3522,3542,3559],"tbody",{},[3462,3484,3485,3491,3494,3496,3499],{},[3486,3487,3488,3489],"td",{},"Hard-coded in ",[18,3490,322],{},[3486,3492,3493],{},"Static file",[3486,3495,1169],{},[3486,3497,3498],{},"No",[3486,3500,3498],{},[3462,3502,3503,3510,3513,3516,3519],{},[3486,3504,3505,344,3508],{},[18,3506,3507],{},"os.environ.get(...)",[18,3509,20],{},[3486,3511,3512],{},"Environment variable",[3486,3514,3515],{},"Full",[3486,3517,3518],{},"Yes",[3486,3520,3521],{},"With loop",[3462,3523,3524,3530,3536,3538,3540],{},[3486,3525,3526,3527],{},"Pydantic ",[18,3528,3529],{},"BaseSettings",[3486,3531,3532,3535],{},[18,3533,3534],{},".env"," + env vars",[3486,3537,3515],{},[3486,3539,3518],{},[3486,3541,3521],{},[3462,3543,3544,3550,3553,3555,3557],{},[3486,3545,3546,3547,2666],{},"Programmatic ",[18,3548,3549],{},"Config",[3486,3551,3552],{},"Python caller",[3486,3554,3515],{},[3486,3556,3518],{},[3486,3558,3518],{},[3462,3560,3561,3567,3570,3573,3576],{},[3486,3562,3563,3564,3566],{},"Separate ",[18,3565,322],{}," per env",[3486,3568,3569],{},"Multiple files",[3486,3571,3572],{},"Good",[3486,3574,3575],{},"Moderate",[3486,3577,3575],{},[14,3579,3580,3581,3583,3584,3586,3587,3589],{},"The ",[18,3582,3507],{}," pattern in ",[18,3585,20],{}," covers the majority of production deployments. Pydantic ",[18,3588,3529],{}," adds validation and type coercion, which is valuable when the application already uses it for other configuration.",[33,3591,3593],{"id":3592},"hybrid-architectures-and-migration-strategies","Hybrid Architectures and Migration Strategies",[271,3595,3597],{"id":3596},"decoupling-migration-engines-from-application-engines","Decoupling migration engines from application engines",[14,3599,3600],{},"In high-availability deployments, the application engine connects to a read-replica-aware connection string or a pgBouncer transaction-mode pooler. Neither is safe for DDL. Migrations must target the primary directly. Use separate environment variables:",[279,3602,3604],{"className":281,"code":3603,"language":283,"meta":284,"style":284},"# env.py\nMIGRATION_DATABASE_URL = os.environ.get(\n    \"MIGRATION_DATABASE_URL\",\n    os.environ.get(\n        \"DATABASE_URL\",\n        \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@primary.db.internal:5432\u002Fappdb\",\n    ),\n)\nconfig.set_main_option(\"sqlalchemy.url\", MIGRATION_DATABASE_URL)\n",[18,3605,3606,3611,3620,3627,3632,3639,3646,3651,3655],{"__ignoreMap":284},[288,3607,3608],{"class":155,"line":290},[288,3609,3610],{"class":293},"# env.py\n",[288,3612,3613,3616,3618],{"class":155,"line":297},[288,3614,3615],{"class":506},"MIGRATION_DATABASE_URL",[288,3617,940],{"class":369},[288,3619,1033],{"class":373},[288,3621,3622,3625],{"class":155,"line":303},[288,3623,3624],{"class":494},"    \"MIGRATION_DATABASE_URL\"",[288,3626,1041],{"class":373},[288,3628,3629],{"class":155,"line":309},[288,3630,3631],{"class":373},"    os.environ.get(\n",[288,3633,3634,3637],{"class":155,"line":393},[288,3635,3636],{"class":494},"        \"DATABASE_URL\"",[288,3638,1041],{"class":373},[288,3640,3641,3644],{"class":155,"line":411},[288,3642,3643],{"class":494},"        \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@primary.db.internal:5432\u002Fappdb\"",[288,3645,1041],{"class":373},[288,3647,3648],{"class":155,"line":470},[288,3649,3650],{"class":373},"    ),\n",[288,3652,3653],{"class":155,"line":485},[288,3654,533],{"class":373},[288,3656,3657,3659,3661,3663,3665],{"class":155,"line":498},[288,3658,1071],{"class":373},[288,3660,1074],{"class":494},[288,3662,564],{"class":373},[288,3664,3615],{"class":506},[288,3666,533],{"class":373},[14,3668,3669,3670,3672,3673,3677,3678,2356],{},"Setting ",[18,3671,3615],{}," to the primary host bypasses read replicas and connection poolers, both of which are incompatible with DDL execution. For deeper coverage of async engine options, see ",[27,3674,3676],{"href":3675},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","Async Engines, Dialects, and Connection Pooling"," and the specific pooling configuration patterns in ",[27,3679,3681],{"href":3680},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools",[271,3683,3685],{"id":3684},"blue-green-and-rolling-deployments","Blue-green and rolling deployments",[14,3687,3688,3689,3692],{},"Migrations that run as a separate Kubernetes Job before the new pod version starts must be idempotent and backward-compatible. Alembic's ",[18,3690,3691],{},"alembic_version"," table guarantees that a migration script runs exactly once per revision ID. To ensure new code is compatible with both the old schema (during the deploy window) and the new schema, apply the expand-contract pattern:",[2386,3694,3695,3702],{},[55,3696,3697,3701],{},[3698,3699,3700],"strong",{},"Expand",": add nullable columns, new tables, new indexes — never remove or rename existing ones.",[55,3703,3704,3707],{},[3698,3705,3706],{},"Contract",": in a subsequent release, remove the old column or constraint once all instances run the new code.",[14,3709,3710,3711,3715],{},"Autogenerate supports both steps. The key is never merging expand and contract into a single migration. Review the autogenerate output carefully before committing — see ",[27,3712,3714],{"href":3713},"\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002F","Autogenerating and Reviewing Migration Scripts"," for the review workflow.",[271,3717,3719],{"id":3718},"connection-pooler-compatibility","Connection pooler compatibility",[14,3721,3722,3723,3726,3727,3730,3731,3734],{},"pgBouncer in transaction-pooling mode breaks ",[18,3724,3725],{},"SET search_path",", advisory locks, and prepared statements. The migration engine should connect directly to PostgreSQL or use pgBouncer in session-pooling mode. Add ",[18,3728,3729],{},"statement_cache_size=0"," to ",[18,3732,3733],{},"connect_args"," when asyncpg connects through any pooler to disable prepared statement caching:",[279,3736,3738],{"className":281,"code":3737,"language":283,"meta":284,"style":284},"connectable = create_async_engine(\n    MIGRATION_DATABASE_URL,\n    echo=False,\n    pool_pre_ping=True,\n    connect_args={\n        \"statement_cache_size\": 0,  # required for pgBouncer transaction mode\n        \"prepared_statement_cache_size\": 0,\n    },\n)\n",[18,3739,3740,3749,3756,3767,3778,3788,3802,3813,3818],{"__ignoreMap":284},[288,3741,3742,3745,3747],{"class":155,"line":290},[288,3743,3744],{"class":373},"connectable ",[288,3746,491],{"class":369},[288,3748,1606],{"class":373},[288,3750,3751,3754],{"class":155,"line":297},[288,3752,3753],{"class":506},"    MIGRATION_DATABASE_URL",[288,3755,1041],{"class":373},[288,3757,3758,3761,3763,3765],{"class":155,"line":303},[288,3759,3760],{"class":524},"    echo",[288,3762,491],{"class":369},[288,3764,572],{"class":506},[288,3766,1041],{"class":373},[288,3768,3769,3772,3774,3776],{"class":155,"line":309},[288,3770,3771],{"class":524},"    pool_pre_ping",[288,3773,491],{"class":369},[288,3775,530],{"class":506},[288,3777,1041],{"class":373},[288,3779,3780,3783,3785],{"class":155,"line":393},[288,3781,3782],{"class":524},"    connect_args",[288,3784,491],{"class":369},[288,3786,3787],{"class":373},"{\n",[288,3789,3790,3793,3795,3797,3799],{"class":155,"line":411},[288,3791,3792],{"class":494},"        \"statement_cache_size\"",[288,3794,1257],{"class":373},[288,3796,127],{"class":506},[288,3798,2791],{"class":373},[288,3800,3801],{"class":293},"# required for pgBouncer transaction mode\n",[288,3803,3804,3807,3809,3811],{"class":155,"line":470},[288,3805,3806],{"class":494},"        \"prepared_statement_cache_size\"",[288,3808,1257],{"class":373},[288,3810,127],{"class":506},[288,3812,1041],{"class":373},[288,3814,3815],{"class":155,"line":485},[288,3816,3817],{"class":373},"    },\n",[288,3819,3820],{"class":155,"line":498},[288,3821,533],{"class":373},[33,3823,3825],{"id":3824},"production-pitfalls-and-anti-patterns","Production Pitfalls and Anti-Patterns",[2386,3827,3828,3842,3873,3901,3917,3935],{},[55,3829,3830,3834,3835,3837,3838,3841],{},[3698,3831,3832],{},[18,3833,3445],{}," — calling ",[18,3836,60],{}," from inside a running FastAPI or Starlette event loop. Fix: invoke Alembic from a thread via ",[18,3839,3840],{},"loop.run_in_executor(None, run_migrations_sync)"," or run migrations as a separate process before the application starts.",[55,3843,3844,3849,3850,3853,3854,3857,3858,3860,3861,3863,3864,3866,3867,3870,3871,2356],{},[3698,3845,3846],{},[18,3847,3848],{},"sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called"," — using a synchronous engine URL (e.g., ",[18,3851,3852],{},"postgresql:\u002F\u002F"," instead of ",[18,3855,3856],{},"postgresql+asyncpg:\u002F\u002F",") inside an async context, or calling an async method from a synchronous context without ",[18,3859,24],{},". Fix: ensure all URLs in ",[18,3862,20],{}," use ",[18,3865,3856],{}," and that ",[18,3868,3869],{},"do_run_migrations"," is only ever called through ",[18,3872,75],{},[55,3874,3875,3880,3881,3883,3884,3887,3888,3890,3891,3894,3895,3897,3898,3900],{},[3698,3876,3877],{},[18,3878,3879],{},"AttributeError: 'coroutine' object has no attribute 'execute'"," — passing an ",[18,3882,67],{}," directly to ",[18,3885,3886],{},"context.configure(connection=...)"," without going through ",[18,3889,24],{},". Fix: always pass the ",[18,3892,3893],{},"connection"," argument that ",[18,3896,24],{}," supplies to the callback; never pass the ",[18,3899,67],{}," itself.",[55,3902,3903,3906,3907,3909,3910,3912,3913,3916],{},[3698,3904,3905],{},"Silent autogenerate misses"," — model modules not imported before ",[18,3908,2546],{},". Alembic sees an empty metadata and generates no operations, silently treating the schema as up-to-date. Fix: import all model modules explicitly in ",[18,3911,859],{}," and verify with ",[18,3914,3915],{},"alembic check"," that detected changes match expectations.",[55,3918,3919,3922,3923,3926,3927,88,3929,344,3932,3934],{},[3698,3920,3921],{},"DDL conflicts from pgBouncer transaction mode"," — ",[18,3924,3925],{},"asyncpg"," prepared statements cached from a previous session are invalid after pgBouncer rotates the underlying connection. Fix: set ",[18,3928,3729],{},[18,3930,3931],{},"prepared_statement_cache_size=0",[18,3933,3733],{}," on the migration engine.",[55,3936,3937,3942,3943,3945,3946,3948,3949,3952,3953,3955,3956,3959],{},[3698,3938,3939],{},[18,3940,3941],{},"alembic.util.exc.CommandError: Can't locate revision identified by '...'"," — running ",[18,3944,2377],{}," against a database whose ",[18,3947,3691],{}," table references a revision that no longer exists in the ",[18,3950,3951],{},"versions\u002F"," directory (common after branch squashing). Fix: inspect the live ",[18,3954,3691],{}," table, stamp the correct revision with ",[18,3957,3958],{},"alembic stamp \u003Crevision>",", and re-run.",[33,3961,3963],{"id":3962},"frequently-asked-questions","Frequently Asked Questions",[14,3965,3966],{},[3698,3967,3968,3969,3971],{},"Why does Alembic need ",[18,3970,24],{}," if asyncpg can handle async natively?",[14,3973,3974,3975,3977,3978,3981,3982,3984,3985,3987],{},"Alembic's migration runner and ",[18,3976,42],{}," were designed for synchronous DBAPI connections. They issue ",[18,3979,3980],{},"cursor.execute()"," calls in a blocking fashion internally. The ",[18,3983,24],{}," method on an ",[18,3986,67],{}," wraps those synchronous calls so they execute in a greenlet, which asyncpg's event loop integration can schedule without blocking. The result is that Alembic's internals do not need modification, and the async driver still performs the actual I\u002FO asynchronously at the socket level.",[14,3989,3990],{},[3698,3991,3992,3993,3995,3996,3998],{},"Can I reuse my application's ",[18,3994,167],{}," instance in ",[18,3997,20],{},"?",[14,4000,4001,4002,344,4004,4006,4007,4010,4011,4014,4015,4018,4019,4021],{},"Not safely. The application engine is created in one event loop (the ASGI server's loop) and holds connections bound to that loop. ",[18,4003,60],{},[18,4005,20],{}," creates a new, separate event loop. Connections from the application engine are invalid in the migration loop and will raise ",[18,4008,4009],{},"RuntimeError"," or ",[18,4012,4013],{},"asyncpg.exceptions.InterfaceError",". Always create a new ",[18,4016,4017],{},"create_async_engine(...)"," instance inside ",[18,4020,2373],{}," and dispose it before returning.",[14,4023,4024],{},[3698,4025,4026,4027,4029],{},"How do I run ",[18,4028,2377],{}," in a Docker Compose setup without a race condition against the database starting?",[14,4031,4032,4033,4036,4037,4040,4041,4044,4045,3446,4047,4049],{},"Use ",[18,4034,4035],{},"depends_on"," with a health check in ",[18,4038,4039],{},"docker-compose.yml"," to ensure the PostgreSQL container is accepting connections before the migration service starts. In the migration entrypoint script, add a ",[18,4042,4043],{},"pg_isready"," loop as a belt-and-suspenders check, then invoke ",[18,4046,2377],{},[18,4048,2401],{}," on the migration engine provides a final connection validation before DDL begins.",[14,4051,4052],{},[3698,4053,4054,4055,4057],{},"Does the async ",[18,4056,20],{}," pattern work with SQLite for testing?",[14,4059,4060,4061,4064,4065,4067,4068,3446,4071,4073,4074,4076],{},"Yes, with the ",[18,4062,4063],{},"aiosqlite"," driver. Replace ",[18,4066,3856],{}," with ",[18,4069,4070],{},"sqlite+aiosqlite:\u002F\u002F\u002F.\u002Ftest.db",[18,4072,24],{}," pattern is identical. For in-memory SQLite databases in test suites, pass the same ",[18,4075,167],{}," instance to both the application fixtures and a custom migration runner so the in-memory database is not discarded between setup steps.",[14,4078,4079],{},[3698,4080,4081,4082,4085],{},"Can ",[18,4083,4084],{},"alembic autogenerate"," detect changes to column types, server defaults, and constraints?",[14,4087,4088,4089,88,4092,4095,4096,4098,4099,4102,4103,2356],{},"Yes, when ",[18,4090,4091],{},"compare_type=True",[18,4093,4094],{},"compare_server_default=True"," are set in ",[18,4097,204],{},". Without these flags, Alembic compares only table and column existence, not type changes. Be aware that ",[18,4100,4101],{},"compare_server_default"," can generate false positives when the database normalizes default expressions differently from how SQLAlchemy emits them. Review autogenerated scripts before applying them in production, as covered in ",[27,4104,3714],{"href":3713},[33,4106,4108],{"id":4107},"related","Related",[2386,4110,4111,4116,4124,4129,4134],{},[55,4112,4113,4115],{},[27,4114,30],{"href":29}," — parent pillar covering the full migration lifecycle",[55,4117,4118,4120,4121,4123],{},[27,4119,2355],{"href":2354}," — step-by-step walkthrough of each ",[18,4122,20],{}," configuration option",[55,4125,4126,4128],{},[27,4127,3714],{"href":3713}," — generating and validating migration scripts from model metadata",[55,4130,4131,4133],{},[27,4132,2537],{"href":2536}," — expand-contract and concurrent index patterns for live deployments",[55,4135,4136,4138],{},[27,4137,3676],{"href":3675}," — engine and pool configuration for runtime async database access",[4140,4141,4142],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .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 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}",{"title":284,"searchDepth":297,"depth":297,"links":4144},[4145,4146,4152,4158,4163,4168,4169,4174,4175,4176],{"id":35,"depth":297,"text":36},{"id":265,"depth":297,"text":4147,"children":4148},"Async Engine Setup and env.py Structure",[4149,4150,4151],{"id":273,"depth":303,"text":274},{"id":332,"depth":303,"text":333},{"id":972,"depth":303,"text":973},{"id":1139,"depth":297,"text":4153,"children":4154},"Complete Async env.py Implementation",[4155,4156,4157],{"id":1146,"depth":303,"text":1147},{"id":1311,"depth":303,"text":1312},{"id":1733,"depth":303,"text":1734},{"id":2359,"depth":297,"text":2360,"children":4159},[4160,4161,4162],{"id":2363,"depth":303,"text":2364},{"id":2405,"depth":303,"text":2406},{"id":2540,"depth":303,"text":2541},{"id":2564,"depth":297,"text":2565,"children":4164},[4165,4166,4167],{"id":2568,"depth":303,"text":2569},{"id":2811,"depth":303,"text":2812},{"id":3189,"depth":303,"text":3190},{"id":3453,"depth":297,"text":3454},{"id":3592,"depth":297,"text":3593,"children":4170},[4171,4172,4173],{"id":3596,"depth":303,"text":3597},{"id":3684,"depth":303,"text":3685},{"id":3718,"depth":303,"text":3719},{"id":3824,"depth":297,"text":3825},{"id":3962,"depth":297,"text":3963},{"id":4107,"depth":297,"text":4108},"Configuring Alembic to drive schema migrations through an async SQLAlchemy engine requires a specific wiring pattern that bridges Alembic's inherently synchronous migration runner with the asyncio event loop. This guide covers the complete async env.py structure, the run_sync bridging technique, environment-based URL configuration, and both online and offline migration modes — all within the broader Alembic Async Migrations and Schema Evolution workflow.","md",{"date":4180},"2026-06-18","\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines",{"title":5,"description":4177},"alembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002Findex","8yHdWNSu_GYCEu9gTHnfiejstUMIxzFutjdlFGMwuOc",1781810028981]