[{"data":1,"prerenderedAt":3395},["ShallowReactive",2],{"page-\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002F":3},{"id":4,"title":5,"body":6,"description":3387,"extension":3388,"meta":3389,"navigation":122,"path":3391,"seo":3392,"stem":3393,"__hash__":3394},"content\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002Findex.md","Autogenerating and Reviewing Migration Scripts with Alembic",{"type":7,"value":8,"toc":3366},"minimark",[9,13,36,41,72,79,136,161,251,258,460,465,530,555,559,562,663,682,686,704,1111,1131,1138,1142,1160,1166,1213,1222,1226,1233,1243,1247,1251,1258,1321,1338,1521,1524,1543,1547,1559,1755,1766,1805,1812,1824,1828,1834,1893,1897,1907,2318,2323,2326,2716,2733,2737,2745,2752,2763,2807,2827,2830,2922,2926,2942,2968,2979,3046,3054,3072,3076,3206,3210,3233,3252,3278,3304,3321,3325,3362],[10,11,5],"h1",{"id":12},"autogenerating-and-reviewing-migration-scripts-with-alembic",[14,15,16,17,21,22,25,26,31,32,35],"p",{},"Alembic's ",[18,19,20],"code",{},"revision --autogenerate"," command compares your SQLAlchemy ",[18,23,24],{},"MetaData"," object against the live database schema and emits a migration script containing the op-directives needed to bring them in sync — part of the ",[27,28,30],"a",{"href":29},"\u002Falembic-async-migrations-and-schema-evolution\u002F","Alembic async migrations and schema evolution"," workflow every production team needs to master. This guide covers the full cycle: wiring ",[18,33,34],{},"target_metadata",", tuning the diff engine, editing generated revisions, writing data migrations, and managing branch graphs.",[37,38,40],"h2",{"id":39},"concept-execution-model","Concept & Execution Model",[14,42,43,44,48,49,52,53,56,57,60,61,64,65,64,68,71],{},"Autogenerate sits at the intersection of two graphs: the ",[45,46,47],"strong",{},"model graph"," (your Python ",[18,50,51],{},"Table"," and ",[18,54,55],{},"DeclarativeBase"," definitions) and the ",[45,58,59],{},"reflected graph"," (the live schema Alembic reads from the database at migration time). The diff engine walks both graphs, column by column and constraint by constraint, and translates differences into ",[18,62,63],{},"op.add_column",", ",[18,66,67],{},"op.drop_index",[18,69,70],{},"op.alter_column",", and similar directives.",[14,73,74,75,78],{},"This works only if ",[18,76,77],{},"env.py"," is wired correctly. The critical line is:",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-python shiki shiki-themes github-light github-dark","# alembic\u002Fenv.py\nfrom myapp.models import Base  # imports all mapped classes via the registry\n\ntarget_metadata = Base.metadata\n","python","",[18,87,88,97,117,124],{"__ignoreMap":85},[89,90,93],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sJ8bj","# alembic\u002Fenv.py\n",[89,98,100,104,108,111,114],{"class":91,"line":99},2,[89,101,103],{"class":102},"szBVR","from",[89,105,107],{"class":106},"sVt8B"," myapp.models ",[89,109,110],{"class":102},"import",[89,112,113],{"class":106}," Base  ",[89,115,116],{"class":95},"# imports all mapped classes via the registry\n",[89,118,120],{"class":91,"line":119},3,[89,121,123],{"emptyLinePlaceholder":122},true,"\n",[89,125,127,130,133],{"class":91,"line":126},4,[89,128,129],{"class":106},"target_metadata ",[89,131,132],{"class":102},"=",[89,134,135],{"class":106}," Base.metadata\n",[14,137,138,139,141,142,145,146,149,150,64,153,156,157,160],{},"Without ",[18,140,34],{},", autogenerate has no model graph to compare against and will emit an empty migration every time. Import the metadata ",[45,143,144],{},"after"," all modules that define tables have been imported — a common mistake is importing ",[18,147,148],{},"Base"," from a module that itself does not import submodules where ",[18,151,152],{},"Order",[18,154,155],{},"Product",", or ",[18,158,159],{},"Tenant"," models live. A robust pattern is a dedicated model loader:",[80,162,164],{"className":82,"code":163,"language":84,"meta":85,"style":85},"# alembic\u002Fenv.py — safe model import pattern\nfrom myapp.models import Base\n# Explicitly import every subpackage so that their Table registrations execute\nimport myapp.models.users      # noqa: F401\nimport myapp.models.orders     # noqa: F401\nimport myapp.models.products   # noqa: F401\nimport myapp.models.invoices   # noqa: F401\nimport myapp.models.tenants    # noqa: F401\n\ntarget_metadata = Base.metadata\n",[18,165,166,171,182,187,197,207,217,227,237,242],{"__ignoreMap":85},[89,167,168],{"class":91,"line":92},[89,169,170],{"class":95},"# alembic\u002Fenv.py — safe model import pattern\n",[89,172,173,175,177,179],{"class":91,"line":99},[89,174,103],{"class":102},[89,176,107],{"class":106},[89,178,110],{"class":102},[89,180,181],{"class":106}," Base\n",[89,183,184],{"class":91,"line":119},[89,185,186],{"class":95},"# Explicitly import every subpackage so that their Table registrations execute\n",[89,188,189,191,194],{"class":91,"line":126},[89,190,110],{"class":102},[89,192,193],{"class":106}," myapp.models.users      ",[89,195,196],{"class":95},"# noqa: F401\n",[89,198,200,202,205],{"class":91,"line":199},5,[89,201,110],{"class":102},[89,203,204],{"class":106}," myapp.models.orders     ",[89,206,196],{"class":95},[89,208,210,212,215],{"class":91,"line":209},6,[89,211,110],{"class":102},[89,213,214],{"class":106}," myapp.models.products   ",[89,216,196],{"class":95},[89,218,220,222,225],{"class":91,"line":219},7,[89,221,110],{"class":102},[89,223,224],{"class":106}," myapp.models.invoices   ",[89,226,196],{"class":95},[89,228,230,232,235],{"class":91,"line":229},8,[89,231,110],{"class":102},[89,233,234],{"class":106}," myapp.models.tenants    ",[89,236,196],{"class":95},[89,238,240],{"class":91,"line":239},9,[89,241,123],{"emptyLinePlaceholder":122},[89,243,245,247,249],{"class":91,"line":244},10,[89,246,129],{"class":106},[89,248,132],{"class":102},[89,250,135],{"class":106},[14,252,253,254,257],{},"The ",[18,255,256],{},"# noqa: F401"," comments suppress linter warnings for unused imports — these imports are intentionally side-effect-only.",[259,260,263],"figure",{"className":261},[262],"diagram",[264,265,270,271,270,275,270,270,279,270,270,287,270,296,270,303,270,306,270,312,270,316,270,320,270,324,270,328,270,332,270,335,270,270,339,270,342,270,346,270,349,270,351,270,353,270,357,270,360,270,362,270,365,270,368,270,270,371,270,378,270,383,270,388,270,392,270,396,270,400,270,270,404,270,270,410,270,270,413,270,419,270,424,270,429,270,270,433,270,437,441,270,444],"svg",{"viewBox":266,"role":267,"ariaLabel":268,"xmlns":269},"0 0 740 420","img","Autogenerate diff pipeline: model metadata vs reflected database schema producing op directives","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[272,273,274],"title",{},"Alembic autogenerate diff pipeline",[276,277,278],"desc",{},"Shows model metadata and reflected DB schema feeding into the diff engine, which outputs op directives written into a migration script file.",[280,281],"rect",{"x":282,"y":282,"width":283,"height":284,"fill":285,"rx":286},"0","740","420","#f1f9f6","8",[280,288],{"x":289,"y":290,"width":291,"height":292,"rx":286,"fill":293,"stroke":294,"style":295},"20","70","185","260","#123a35","#0f766e","stroke-width:2",[297,298,302],"text",{"x":299,"y":300,"fill":285,"style":301},"112","98","text-anchor:middle;font-size:13px;font-weight:bold","SQLAlchemy",[297,304,24],{"x":299,"y":305,"fill":285,"style":301},"116",[297,307,311],{"x":299,"y":308,"fill":309,"style":310},"144","#a7d3cd","text-anchor:middle;font-size:11px","Table: users",[297,313,315],{"x":299,"y":314,"fill":309,"style":310},"162","Table: orders",[297,317,319],{"x":299,"y":318,"fill":309,"style":310},"180","Table: products",[297,321,323],{"x":299,"y":322,"fill":309,"style":310},"198","Table: invoices",[297,325,327],{"x":299,"y":326,"fill":309,"style":310},"216","ix_user_email",[297,329,331],{"x":299,"y":330,"fill":309,"style":310},"234","FK: orders→users",[297,333,34],{"x":299,"y":292,"fill":334,"style":310},"#6db8b0",[297,336,338],{"x":299,"y":337,"fill":334,"style":310},"278","Base.metadata",[280,340],{"x":341,"y":290,"width":291,"height":292,"rx":286,"fill":293,"stroke":294,"style":295},"515",[297,343,345],{"x":344,"y":300,"fill":285,"style":301},"607","Live Database",[297,347,348],{"x":344,"y":305,"fill":285,"style":301},"Schema",[297,350,311],{"x":344,"y":308,"fill":309,"style":310},[297,352,315],{"x":344,"y":314,"fill":309,"style":310},[297,354,356],{"x":344,"y":318,"fill":355,"style":310},"#f4a261","products absent ✗",[297,358,359],{"x":344,"y":322,"fill":355,"style":310},"invoices absent ✗",[297,361,327],{"x":344,"y":326,"fill":309,"style":310},[297,363,364],{"x":344,"y":330,"fill":355,"style":310},"FK missing ✗",[297,366,367],{"x":344,"y":292,"fill":334,"style":310},"inspector.reflect",[297,369,370],{"x":344,"y":337,"fill":334,"style":310},"dialect introspect",[280,372],{"x":373,"y":374,"width":375,"height":376,"rx":286,"fill":294,"stroke":377,"style":295},"258","130","204","140","#1f9f95",[297,379,382],{"x":380,"y":381,"fill":285,"style":301},"360","158","Diff Engine",[297,384,387],{"x":380,"y":385,"fill":386,"style":310},"178","#d1f0ec","compare_type=True",[297,389,391],{"x":380,"y":390,"fill":386,"style":310},"196","compare_server_default",[297,393,395],{"x":380,"y":394,"fill":386,"style":310},"214","naming_convention",[297,397,399],{"x":380,"y":398,"fill":386,"style":310},"232","include_object hook",[297,401,403],{"x":380,"y":402,"fill":386,"style":310},"250","include_schemas",[91,405],{"x1":406,"y1":407,"x2":408,"y2":407,"stroke":377,"style":409},"205","200","256","stroke-width:2;marker-end:url(#arrowhead)",[91,411],{"x1":341,"y1":407,"x2":412,"y2":407,"stroke":377,"style":409},"464",[280,414],{"x":415,"y":416,"width":417,"height":418,"rx":286,"fill":285,"stroke":294,"style":295},"210","330","300","65",[297,420,423],{"x":380,"y":421,"fill":422,"style":301},"354","#113f39","Migration Script",[297,425,428],{"x":380,"y":426,"fill":427,"style":310},"373","#3f4f4b","op.create_table(\"products\", ...)",[297,430,432],{"x":380,"y":431,"fill":427,"style":310},"389","op.add_column \u002F op.create_foreign_key",[91,434],{"x1":380,"y1":435,"x2":380,"y2":436,"stroke":377,"style":409},"270","328",[297,438,440],{"x":299,"y":439,"fill":427,"style":310},"350","\nmodel graph\n",[297,442,443],{"x":344,"y":439,"fill":427,"style":310},"\nreflected graph\n",[445,446,447,448,270],"defs",{},"\n    ",[449,450,455,456,447],"marker",{"id":451,"markerWidth":286,"markerHeight":286,"refX":452,"refY":453,"orient":454},"arrowhead","6","3","auto","\n      ",[457,458],"path",{"d":459,"fill":377},"M0,0 L0,6 L8,3 z",[461,462,464],"h3",{"id":463},"running-autogenerate","Running Autogenerate",[80,466,470],{"className":467,"code":468,"language":469,"meta":85,"style":85},"language-bash shiki shiki-themes github-light github-dark","# Generate a named revision against the configured database URL\nalembic revision --autogenerate -m \"add_products_table_and_orders_fk\"\n\n# Override the URL without editing alembic.ini\nalembic revision --autogenerate -m \"add_invoices\" \\\n    --url postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Fappdb\n","bash",[18,471,472,477,497,501,506,522],{"__ignoreMap":85},[89,473,474],{"class":91,"line":92},[89,475,476],{"class":95},"# Generate a named revision against the configured database URL\n",[89,478,479,483,487,491,494],{"class":91,"line":99},[89,480,482],{"class":481},"sScJk","alembic",[89,484,486],{"class":485},"sZZnC"," revision",[89,488,490],{"class":489},"sj4cs"," --autogenerate",[89,492,493],{"class":489}," -m",[89,495,496],{"class":485}," \"add_products_table_and_orders_fk\"\n",[89,498,499],{"class":91,"line":119},[89,500,123],{"emptyLinePlaceholder":122},[89,502,503],{"class":91,"line":126},[89,504,505],{"class":95},"# Override the URL without editing alembic.ini\n",[89,507,508,510,512,514,516,519],{"class":91,"line":199},[89,509,482],{"class":481},[89,511,486],{"class":485},[89,513,490],{"class":489},[89,515,493],{"class":489},[89,517,518],{"class":485}," \"add_invoices\"",[89,520,521],{"class":489}," \\\n",[89,523,524,527],{"class":91,"line":209},[89,525,526],{"class":489},"    --url",[89,528,529],{"class":485}," postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Fappdb\n",[14,531,532,533,536,537,539,540,543,544,547,548,52,551,554],{},"Alembic connects to the database URL configured in ",[18,534,535],{},"alembic.ini",", reflects the schema, diffs against ",[18,538,34],{},", and writes a new file under ",[18,541,542],{},"alembic\u002Fversions\u002F",". The filename starts with a random hex token — that token is the ",[45,545,546],{},"revision identifier"," used in ",[18,549,550],{},"depends_on",[18,552,553],{},"down_revision"," chains. Never rename or re-token a revision file after committing it; doing so severs the version chain for anyone who has already applied it.",[461,556,558],{"id":557},"what-autogenerate-can-and-cannot-detect","What Autogenerate Can and Cannot Detect",[14,560,561],{},"Understanding the detection boundaries prevents nasty surprises in production.",[563,564,565,578],"table",{},[566,567,568],"thead",{},[569,570,571,575],"tr",{},[572,573,574],"th",{},"Detected automatically",[572,576,577],{},"Requires flag or manual step",[579,580,581,590,602,614,622,630,642,653],"tbody",{},[569,582,583,587],{},[584,585,586],"td",{},"New and dropped tables",[584,588,589],{},"Stored procedures, functions, triggers, views",[569,591,592,595],{},[584,593,594],{},"New, dropped, and modified columns",[584,596,597,598,601],{},"Postgres ",[18,599,600],{},"SEQUENCE"," objects",[569,603,604,607],{},[584,605,606],{},"Primary key changes",[584,608,609,610,613],{},"Partial indexes (",[18,611,612],{},"WHERE"," clause)",[569,615,616,619],{},[584,617,618],{},"Foreign key additions and drops",[584,620,621],{},"Column-level comments",[569,623,624,627],{},[584,625,626],{},"Index additions and drops",[584,628,629],{},"Schema creation \u002F drop",[569,631,632,635],{},[584,633,634],{},"Unique constraint changes",[584,636,637,638,641],{},"Enum type bodies (",[18,639,640],{},"CREATE TYPE",")",[569,643,644,650],{},[584,645,646,647,641],{},"Server default changes (",[18,648,649],{},"compare_server_default=True",[584,651,652],{},"Row-level data changes",[569,654,655,660],{},[584,656,657,658,641],{},"Column type changes (",[18,659,387],{},[584,661,662],{},"Any dialect-specific extension objects",[14,664,665,666,669,670,673,674,677,678,681],{},"This breakdown is stable across SQLAlchemy 2.0 and Alembic 1.13+. For partial index detection, you can write a custom ",[18,667,668],{},"render_item"," hook that emits a raw ",[18,671,672],{},"op.execute(\"CREATE INDEX ... WHERE ...\")"," instead of using ",[18,675,676],{},"op.create_index",". That hook must also handle the corresponding ",[18,679,680],{},"downgrade"," path manually.",[37,683,685],{"id":684},"query-construction-async-execution-patterns","Query Construction & Async Execution Patterns",[14,687,688,689,64,693,695,696,699,700,703],{},"Because autogenerate needs to reflect the live schema, it requires a database connection. In async projects wired through ",[27,690,692],{"href":691},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002F","configuring Alembic with async SQLAlchemy engines",[18,694,77],{}," must run the reflection inside ",[18,697,698],{},"asyncio.run()"," using ",[18,701,702],{},"run_sync",":",[80,705,707],{"className":82,"code":706,"language":84,"meta":85,"style":85},"# alembic\u002Fenv.py — complete async env.py for autogenerate\nimport asyncio\nfrom logging.config import fileConfig\n\nfrom alembic import context\nfrom sqlalchemy.ext.asyncio import async_engine_from_config\n\n# Side-effect imports: register all ORM models with Base.metadata\nfrom myapp.models import Base          # noqa: F401\nimport myapp.models.users              # noqa: F401\nimport myapp.models.orders             # noqa: F401\nimport myapp.models.products           # noqa: F401\nimport myapp.models.invoices           # noqa: F401\n\nconfig = context.config\nfileConfig(config.config_file_name)\ntarget_metadata = Base.metadata\n\n\ndef do_run_migrations(connection):\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 = async_engine_from_config(\n        config.get_section(config.config_ini_section),\n        prefix=\"sqlalchemy.\",\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\nrun_migrations_online()\n",[18,708,709,714,721,733,737,749,761,765,770,783,792,802,812,822,827,838,844,853,858,863,875,881,893,904,918,930,942,948,957,963,968,973,994,1005,1011,1024,1029,1047,1056,1065,1070,1075,1089,1095,1100,1105],{"__ignoreMap":85},[89,710,711],{"class":91,"line":92},[89,712,713],{"class":95},"# alembic\u002Fenv.py — complete async env.py for autogenerate\n",[89,715,716,718],{"class":91,"line":99},[89,717,110],{"class":102},[89,719,720],{"class":106}," asyncio\n",[89,722,723,725,728,730],{"class":91,"line":119},[89,724,103],{"class":102},[89,726,727],{"class":106}," logging.config ",[89,729,110],{"class":102},[89,731,732],{"class":106}," fileConfig\n",[89,734,735],{"class":91,"line":126},[89,736,123],{"emptyLinePlaceholder":122},[89,738,739,741,744,746],{"class":91,"line":199},[89,740,103],{"class":102},[89,742,743],{"class":106}," alembic ",[89,745,110],{"class":102},[89,747,748],{"class":106}," context\n",[89,750,751,753,756,758],{"class":91,"line":209},[89,752,103],{"class":102},[89,754,755],{"class":106}," sqlalchemy.ext.asyncio ",[89,757,110],{"class":102},[89,759,760],{"class":106}," async_engine_from_config\n",[89,762,763],{"class":91,"line":219},[89,764,123],{"emptyLinePlaceholder":122},[89,766,767],{"class":91,"line":229},[89,768,769],{"class":95},"# Side-effect imports: register all ORM models with Base.metadata\n",[89,771,772,774,776,778,781],{"class":91,"line":239},[89,773,103],{"class":102},[89,775,107],{"class":106},[89,777,110],{"class":102},[89,779,780],{"class":106}," Base          ",[89,782,196],{"class":95},[89,784,785,787,790],{"class":91,"line":244},[89,786,110],{"class":102},[89,788,789],{"class":106}," myapp.models.users              ",[89,791,196],{"class":95},[89,793,795,797,800],{"class":91,"line":794},11,[89,796,110],{"class":102},[89,798,799],{"class":106}," myapp.models.orders             ",[89,801,196],{"class":95},[89,803,805,807,810],{"class":91,"line":804},12,[89,806,110],{"class":102},[89,808,809],{"class":106}," myapp.models.products           ",[89,811,196],{"class":95},[89,813,815,817,820],{"class":91,"line":814},13,[89,816,110],{"class":102},[89,818,819],{"class":106}," myapp.models.invoices           ",[89,821,196],{"class":95},[89,823,825],{"class":91,"line":824},14,[89,826,123],{"emptyLinePlaceholder":122},[89,828,830,833,835],{"class":91,"line":829},15,[89,831,832],{"class":106},"config ",[89,834,132],{"class":102},[89,836,837],{"class":106}," context.config\n",[89,839,841],{"class":91,"line":840},16,[89,842,843],{"class":106},"fileConfig(config.config_file_name)\n",[89,845,847,849,851],{"class":91,"line":846},17,[89,848,129],{"class":106},[89,850,132],{"class":102},[89,852,135],{"class":106},[89,854,856],{"class":91,"line":855},18,[89,857,123],{"emptyLinePlaceholder":122},[89,859,861],{"class":91,"line":860},19,[89,862,123],{"emptyLinePlaceholder":122},[89,864,866,869,872],{"class":91,"line":865},20,[89,867,868],{"class":102},"def",[89,870,871],{"class":481}," do_run_migrations",[89,873,874],{"class":106},"(connection):\n",[89,876,878],{"class":91,"line":877},21,[89,879,880],{"class":106},"    context.configure(\n",[89,882,884,888,890],{"class":91,"line":883},22,[89,885,887],{"class":886},"s4XuR","        connection",[89,889,132],{"class":102},[89,891,892],{"class":106},"connection,\n",[89,894,896,899,901],{"class":91,"line":895},23,[89,897,898],{"class":886},"        target_metadata",[89,900,132],{"class":102},[89,902,903],{"class":106},"target_metadata,\n",[89,905,907,910,912,915],{"class":91,"line":906},24,[89,908,909],{"class":886},"        compare_type",[89,911,132],{"class":102},[89,913,914],{"class":489},"True",[89,916,917],{"class":106},",\n",[89,919,921,924,926,928],{"class":91,"line":920},25,[89,922,923],{"class":886},"        compare_server_default",[89,925,132],{"class":102},[89,927,914],{"class":489},[89,929,917],{"class":106},[89,931,933,936,938,940],{"class":91,"line":932},26,[89,934,935],{"class":886},"        include_schemas",[89,937,132],{"class":102},[89,939,914],{"class":489},[89,941,917],{"class":106},[89,943,945],{"class":91,"line":944},27,[89,946,947],{"class":106},"    )\n",[89,949,951,954],{"class":91,"line":950},28,[89,952,953],{"class":102},"    with",[89,955,956],{"class":106}," context.begin_transaction():\n",[89,958,960],{"class":91,"line":959},29,[89,961,962],{"class":106},"        context.run_migrations()\n",[89,964,966],{"class":91,"line":965},30,[89,967,123],{"emptyLinePlaceholder":122},[89,969,971],{"class":91,"line":970},31,[89,972,123],{"emptyLinePlaceholder":122},[89,974,976,979,982,985,988,991],{"class":91,"line":975},32,[89,977,978],{"class":102},"async",[89,980,981],{"class":102}," def",[89,983,984],{"class":481}," run_async_migrations",[89,986,987],{"class":106},"() -> ",[89,989,990],{"class":489},"None",[89,992,993],{"class":106},":\n",[89,995,997,1000,1002],{"class":91,"line":996},33,[89,998,999],{"class":106},"    connectable ",[89,1001,132],{"class":102},[89,1003,1004],{"class":106}," async_engine_from_config(\n",[89,1006,1008],{"class":91,"line":1007},34,[89,1009,1010],{"class":106},"        config.get_section(config.config_ini_section),\n",[89,1012,1014,1017,1019,1022],{"class":91,"line":1013},35,[89,1015,1016],{"class":886},"        prefix",[89,1018,132],{"class":102},[89,1020,1021],{"class":485},"\"sqlalchemy.\"",[89,1023,917],{"class":106},[89,1025,1027],{"class":91,"line":1026},36,[89,1028,947],{"class":106},[89,1030,1032,1035,1038,1041,1044],{"class":91,"line":1031},37,[89,1033,1034],{"class":102},"    async",[89,1036,1037],{"class":102}," with",[89,1039,1040],{"class":106}," connectable.connect() ",[89,1042,1043],{"class":102},"as",[89,1045,1046],{"class":106}," connection:\n",[89,1048,1050,1053],{"class":91,"line":1049},38,[89,1051,1052],{"class":102},"        await",[89,1054,1055],{"class":106}," connection.run_sync(do_run_migrations)\n",[89,1057,1059,1062],{"class":91,"line":1058},39,[89,1060,1061],{"class":102},"    await",[89,1063,1064],{"class":106}," connectable.dispose()\n",[89,1066,1068],{"class":91,"line":1067},40,[89,1069,123],{"emptyLinePlaceholder":122},[89,1071,1073],{"class":91,"line":1072},41,[89,1074,123],{"emptyLinePlaceholder":122},[89,1076,1078,1080,1083,1085,1087],{"class":91,"line":1077},42,[89,1079,868],{"class":102},[89,1081,1082],{"class":481}," run_migrations_online",[89,1084,987],{"class":106},[89,1086,990],{"class":489},[89,1088,993],{"class":106},[89,1090,1092],{"class":91,"line":1091},43,[89,1093,1094],{"class":106},"    asyncio.run(run_async_migrations())\n",[89,1096,1098],{"class":91,"line":1097},44,[89,1099,123],{"emptyLinePlaceholder":122},[89,1101,1103],{"class":91,"line":1102},45,[89,1104,123],{"emptyLinePlaceholder":122},[89,1106,1108],{"class":91,"line":1107},46,[89,1109,1110],{"class":106},"run_migrations_online()\n",[14,1112,253,1113,1115,1116,1119,1120,1123,1124,1126,1127,1130],{},[18,1114,702],{}," call hands a synchronous DBAPI connection to Alembic's reflection machinery, which does not know about async. This is the canonical pattern — attempting to call ",[18,1117,1118],{},"context.run_migrations()"," directly inside an ",[18,1121,1122],{},"async def"," without ",[18,1125,702],{}," raises ",[18,1128,1129],{},"sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called",".",[14,1132,1133,1134,1137],{},"For offline mode (generating SQL without a live database connection), the async engine is unavailable. Autogenerate requires online mode; offline mode is only suitable for ",[18,1135,1136],{},"alembic upgrade --sql"," to generate a SQL script from an already-generated revision.",[37,1139,1141],{"id":1140},"state-management-session-boundaries","State Management & Session Boundaries",[14,1143,1144,1145,1147,1148,1151,1152,1154,1155,1159],{},"Autogenerate reads schema state at the moment the connection is made. If your application uses multiple ",[18,1146,24],{}," objects — for example, one per tenant schema or one for a read model — you must either merge them or use ",[18,1149,1150],{},"include_object"," \u002F ",[18,1153,403],{}," filters (covered fully in the ",[27,1156,1158],{"href":1157},"\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002Fexcluding-tables-and-schemas-from-alembic-autogenerate\u002F","excluding tables and schemas guide",") to keep Alembic from generating spurious drop directives for tables it does not own.",[14,1161,1162,1163,1165],{},"To target multiple ",[18,1164,24],{}," objects in a single autogenerate run, pass them as a list:",[80,1167,1169],{"className":82,"code":1168,"language":84,"meta":85,"style":85},"# alembic\u002Fenv.py — multiple MetaData objects\nfrom myapp.app_models import AppBase\nfrom myapp.admin_models import AdminBase\n\ntarget_metadata = [AppBase.metadata, AdminBase.metadata]\n",[18,1170,1171,1176,1188,1200,1204],{"__ignoreMap":85},[89,1172,1173],{"class":91,"line":92},[89,1174,1175],{"class":95},"# alembic\u002Fenv.py — multiple MetaData objects\n",[89,1177,1178,1180,1183,1185],{"class":91,"line":99},[89,1179,103],{"class":102},[89,1181,1182],{"class":106}," myapp.app_models ",[89,1184,110],{"class":102},[89,1186,1187],{"class":106}," AppBase\n",[89,1189,1190,1192,1195,1197],{"class":91,"line":119},[89,1191,103],{"class":102},[89,1193,1194],{"class":106}," myapp.admin_models ",[89,1196,110],{"class":102},[89,1198,1199],{"class":106}," AdminBase\n",[89,1201,1202],{"class":91,"line":126},[89,1203,123],{"emptyLinePlaceholder":122},[89,1205,1206,1208,1210],{"class":91,"line":199},[89,1207,129],{"class":106},[89,1209,132],{"class":102},[89,1211,1212],{"class":106}," [AppBase.metadata, AdminBase.metadata]\n",[14,1214,1215,1216,1218,1219,1221],{},"Alembic 1.7+ supports this. Each ",[18,1217,24],{}," object must use the same ",[18,1220,395],{}," or constraint detection will produce unpredictable results — the diff engine compares constraint names, so inconsistent conventions produce false positives.",[461,1223,1225],{"id":1224},"transaction-boundaries-for-data-migrations","Transaction Boundaries for Data Migrations",[14,1227,1228,1229,1232],{},"Alembic wraps each migration in a transaction by default (",[18,1230,1231],{},"transaction_per_migration=True","). If you run DDL and DML together — creating a column and then back-filling values — both must succeed atomically or the schema ends up in a partially migrated state. PostgreSQL supports transactional DDL, so this works cleanly. MySQL does not; on MySQL separate the DDL and DML into distinct revisions.",[14,1234,253,1235,1238,1239,1242],{},[18,1236,1237],{},"op.get_bind()"," function returns the DBAPI connection bound to the current migration context. Use it inside ",[18,1240,1241],{},"upgrade()"," to execute raw SQL or SQLAlchemy Core expressions for data work. Avoid importing your ORM models directly inside migration files — model definitions change over time, but migrations are frozen at a specific schema state.",[37,1244,1246],{"id":1245},"advanced-autogenerate-patterns","Advanced Autogenerate Patterns",[461,1248,1250],{"id":1249},"compare_type-and-compare_server_default","compare_type and compare_server_default",[14,1252,1253,1254,1257],{},"By default, Alembic does ",[45,1255,1256],{},"not"," compare column types or server defaults, because type comparison across different database dialects is lossy. Enable both flags when you need fine-grained drift detection:",[80,1259,1261],{"className":82,"code":1260,"language":84,"meta":85,"style":85},"context.configure(\n    connection=connection,\n    target_metadata=target_metadata,\n    compare_type=True,            # detect VARCHAR(100) → VARCHAR(255) changes\n    compare_server_default=True,  # detect DEFAULT now() → DEFAULT CURRENT_TIMESTAMP\n)\n",[18,1262,1263,1268,1277,1286,1301,1316],{"__ignoreMap":85},[89,1264,1265],{"class":91,"line":92},[89,1266,1267],{"class":106},"context.configure(\n",[89,1269,1270,1273,1275],{"class":91,"line":99},[89,1271,1272],{"class":886},"    connection",[89,1274,132],{"class":102},[89,1276,892],{"class":106},[89,1278,1279,1282,1284],{"class":91,"line":119},[89,1280,1281],{"class":886},"    target_metadata",[89,1283,132],{"class":102},[89,1285,903],{"class":106},[89,1287,1288,1291,1293,1295,1298],{"class":91,"line":126},[89,1289,1290],{"class":886},"    compare_type",[89,1292,132],{"class":102},[89,1294,914],{"class":489},[89,1296,1297],{"class":106},",            ",[89,1299,1300],{"class":95},"# detect VARCHAR(100) → VARCHAR(255) changes\n",[89,1302,1303,1306,1308,1310,1313],{"class":91,"line":199},[89,1304,1305],{"class":886},"    compare_server_default",[89,1307,132],{"class":102},[89,1309,914],{"class":489},[89,1311,1312],{"class":106},",  ",[89,1314,1315],{"class":95},"# detect DEFAULT now() → DEFAULT CURRENT_TIMESTAMP\n",[89,1317,1318],{"class":91,"line":209},[89,1319,1320],{"class":106},")\n",[14,1322,1323,1325,1326,1329,1330,1333,1334,1337],{},[18,1324,387],{}," triggers ",[18,1327,1328],{},"dialect.compare_type(inspector_column, metadata_column)"," — a method some dialects implement incompletely. For custom ",[18,1331,1332],{},"TypeDecorator"," subclasses, override ",[18,1335,1336],{},"__repr__"," so the string comparison produces stable output:",[80,1339,1341],{"className":82,"code":1340,"language":84,"meta":85,"style":85},"# myapp\u002Ftypes.py\nimport sqlalchemy as sa\n\n\nclass TenantId(sa.TypeDecorator):\n    \"\"\"UUID stored as a VARCHAR(36) with tenant isolation semantics.\"\"\"\n\n    impl = sa.String(36)\n    cache_ok = True\n\n    def __repr__(self) -> str:\n        # Stable repr prevents autogenerate from treating every column as changed\n        return \"TenantId()\"\n\n    def process_bind_param(self, value, dialect):\n        return str(value) if value is not None else None\n\n    def process_result_value(self, value, dialect):\n        return value\n",[18,1342,1343,1348,1360,1364,1368,1389,1394,1398,1413,1423,1427,1443,1448,1456,1460,1470,1501,1505,1514],{"__ignoreMap":85},[89,1344,1345],{"class":91,"line":92},[89,1346,1347],{"class":95},"# myapp\u002Ftypes.py\n",[89,1349,1350,1352,1355,1357],{"class":91,"line":99},[89,1351,110],{"class":102},[89,1353,1354],{"class":106}," sqlalchemy ",[89,1356,1043],{"class":102},[89,1358,1359],{"class":106}," sa\n",[89,1361,1362],{"class":91,"line":119},[89,1363,123],{"emptyLinePlaceholder":122},[89,1365,1366],{"class":91,"line":126},[89,1367,123],{"emptyLinePlaceholder":122},[89,1369,1370,1373,1376,1379,1382,1384,1386],{"class":91,"line":199},[89,1371,1372],{"class":102},"class",[89,1374,1375],{"class":481}," TenantId",[89,1377,1378],{"class":106},"(",[89,1380,1381],{"class":481},"sa",[89,1383,1130],{"class":106},[89,1385,1332],{"class":481},[89,1387,1388],{"class":106},"):\n",[89,1390,1391],{"class":91,"line":209},[89,1392,1393],{"class":485},"    \"\"\"UUID stored as a VARCHAR(36) with tenant isolation semantics.\"\"\"\n",[89,1395,1396],{"class":91,"line":219},[89,1397,123],{"emptyLinePlaceholder":122},[89,1399,1400,1403,1405,1408,1411],{"class":91,"line":229},[89,1401,1402],{"class":106},"    impl ",[89,1404,132],{"class":102},[89,1406,1407],{"class":106}," sa.String(",[89,1409,1410],{"class":489},"36",[89,1412,1320],{"class":106},[89,1414,1415,1418,1420],{"class":91,"line":239},[89,1416,1417],{"class":106},"    cache_ok ",[89,1419,132],{"class":102},[89,1421,1422],{"class":489}," True\n",[89,1424,1425],{"class":91,"line":244},[89,1426,123],{"emptyLinePlaceholder":122},[89,1428,1429,1432,1435,1438,1441],{"class":91,"line":794},[89,1430,1431],{"class":102},"    def",[89,1433,1434],{"class":489}," __repr__",[89,1436,1437],{"class":106},"(self) -> ",[89,1439,1440],{"class":489},"str",[89,1442,993],{"class":106},[89,1444,1445],{"class":91,"line":804},[89,1446,1447],{"class":95},"        # Stable repr prevents autogenerate from treating every column as changed\n",[89,1449,1450,1453],{"class":91,"line":814},[89,1451,1452],{"class":102},"        return",[89,1454,1455],{"class":485}," \"TenantId()\"\n",[89,1457,1458],{"class":91,"line":824},[89,1459,123],{"emptyLinePlaceholder":122},[89,1461,1462,1464,1467],{"class":91,"line":829},[89,1463,1431],{"class":102},[89,1465,1466],{"class":481}," process_bind_param",[89,1468,1469],{"class":106},"(self, value, dialect):\n",[89,1471,1472,1474,1477,1480,1483,1486,1489,1492,1495,1498],{"class":91,"line":840},[89,1473,1452],{"class":102},[89,1475,1476],{"class":489}," str",[89,1478,1479],{"class":106},"(value) ",[89,1481,1482],{"class":102},"if",[89,1484,1485],{"class":106}," value ",[89,1487,1488],{"class":102},"is",[89,1490,1491],{"class":102}," not",[89,1493,1494],{"class":489}," None",[89,1496,1497],{"class":102}," else",[89,1499,1500],{"class":489}," None\n",[89,1502,1503],{"class":91,"line":846},[89,1504,123],{"emptyLinePlaceholder":122},[89,1506,1507,1509,1512],{"class":91,"line":855},[89,1508,1431],{"class":102},[89,1510,1511],{"class":481}," process_result_value",[89,1513,1469],{"class":106},[89,1515,1516,1518],{"class":91,"line":860},[89,1517,1452],{"class":102},[89,1519,1520],{"class":106}," value\n",[14,1522,1523],{},"Verify idempotency by running autogenerate twice in a row — the second run must emit no changes. If it does, the type repr is not stable.",[14,1525,1526,1528,1529,1532,1533,1536,1537,1540,1541,1130],{},[18,1527,649],{}," is similarly fragile for expressions. PostgreSQL renders ",[18,1530,1531],{},"nextval('seq'::regclass)"," while your model might declare ",[18,1534,1535],{},"server_default=text(\"nextval('seq')\")",". Use a custom ",[18,1538,1539],{},"process_revision_directives"," hook to normalize both sides before comparison, or simply exclude known-unstable columns from comparison using ",[18,1542,1150],{},[461,1544,1546],{"id":1545},"metadata-naming-conventions","MetaData Naming Conventions",[14,1548,1549,1550,1552,1553,1555,1556,1558],{},"Without a ",[18,1551,395],{},", Alembic cannot reliably detect renamed constraints because the database assigns auto-generated names that differ from what your model implies. Set a ",[18,1554,395],{}," on your ",[18,1557,24],{}," object and Alembic will construct predictable names it can compare:",[80,1560,1562],{"className":82,"code":1561,"language":84,"meta":85,"style":85},"# myapp\u002Fmodels.py\nfrom sqlalchemy import MetaData\nfrom sqlalchemy.orm import DeclarativeBase\n\nNAMING_CONVENTION = {\n    \"ix\": \"ix_%(column_0_label)s\",\n    \"uq\": \"uq_%(table_name)s_%(column_0_name)s\",\n    \"ck\": \"ck_%(table_name)s_%(constraint_name)s\",\n    \"fk\": \"fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s\",\n    \"pk\": \"pk_%(table_name)s\",\n}\n\n\nclass Base(DeclarativeBase):\n    metadata = MetaData(naming_convention=NAMING_CONVENTION)\n",[18,1563,1564,1569,1580,1592,1596,1607,1626,1649,1670,1695,1711,1716,1720,1724,1737],{"__ignoreMap":85},[89,1565,1566],{"class":91,"line":92},[89,1567,1568],{"class":95},"# myapp\u002Fmodels.py\n",[89,1570,1571,1573,1575,1577],{"class":91,"line":99},[89,1572,103],{"class":102},[89,1574,1354],{"class":106},[89,1576,110],{"class":102},[89,1578,1579],{"class":106}," MetaData\n",[89,1581,1582,1584,1587,1589],{"class":91,"line":119},[89,1583,103],{"class":102},[89,1585,1586],{"class":106}," sqlalchemy.orm ",[89,1588,110],{"class":102},[89,1590,1591],{"class":106}," DeclarativeBase\n",[89,1593,1594],{"class":91,"line":126},[89,1595,123],{"emptyLinePlaceholder":122},[89,1597,1598,1601,1604],{"class":91,"line":199},[89,1599,1600],{"class":489},"NAMING_CONVENTION",[89,1602,1603],{"class":102}," =",[89,1605,1606],{"class":106}," {\n",[89,1608,1609,1612,1615,1618,1621,1624],{"class":91,"line":209},[89,1610,1611],{"class":485},"    \"ix\"",[89,1613,1614],{"class":106},": ",[89,1616,1617],{"class":485},"\"ix_",[89,1619,1620],{"class":489},"%(column_0_label)s",[89,1622,1623],{"class":485},"\"",[89,1625,917],{"class":106},[89,1627,1628,1631,1633,1636,1639,1642,1645,1647],{"class":91,"line":219},[89,1629,1630],{"class":485},"    \"uq\"",[89,1632,1614],{"class":106},[89,1634,1635],{"class":485},"\"uq_",[89,1637,1638],{"class":489},"%(table_name)s",[89,1640,1641],{"class":485},"_",[89,1643,1644],{"class":489},"%(column_0_name)s",[89,1646,1623],{"class":485},[89,1648,917],{"class":106},[89,1650,1651,1654,1656,1659,1661,1663,1666,1668],{"class":91,"line":229},[89,1652,1653],{"class":485},"    \"ck\"",[89,1655,1614],{"class":106},[89,1657,1658],{"class":485},"\"ck_",[89,1660,1638],{"class":489},[89,1662,1641],{"class":485},[89,1664,1665],{"class":489},"%(constraint_name)s",[89,1667,1623],{"class":485},[89,1669,917],{"class":106},[89,1671,1672,1675,1677,1680,1682,1684,1686,1688,1691,1693],{"class":91,"line":239},[89,1673,1674],{"class":485},"    \"fk\"",[89,1676,1614],{"class":106},[89,1678,1679],{"class":485},"\"fk_",[89,1681,1638],{"class":489},[89,1683,1641],{"class":485},[89,1685,1644],{"class":489},[89,1687,1641],{"class":485},[89,1689,1690],{"class":489},"%(referred_table_name)s",[89,1692,1623],{"class":485},[89,1694,917],{"class":106},[89,1696,1697,1700,1702,1705,1707,1709],{"class":91,"line":244},[89,1698,1699],{"class":485},"    \"pk\"",[89,1701,1614],{"class":106},[89,1703,1704],{"class":485},"\"pk_",[89,1706,1638],{"class":489},[89,1708,1623],{"class":485},[89,1710,917],{"class":106},[89,1712,1713],{"class":91,"line":794},[89,1714,1715],{"class":106},"}\n",[89,1717,1718],{"class":91,"line":804},[89,1719,123],{"emptyLinePlaceholder":122},[89,1721,1722],{"class":91,"line":814},[89,1723,123],{"emptyLinePlaceholder":122},[89,1725,1726,1728,1731,1733,1735],{"class":91,"line":824},[89,1727,1372],{"class":102},[89,1729,1730],{"class":481}," Base",[89,1732,1378],{"class":106},[89,1734,55],{"class":481},[89,1736,1388],{"class":106},[89,1738,1739,1742,1744,1747,1749,1751,1753],{"class":91,"line":829},[89,1740,1741],{"class":106},"    metadata ",[89,1743,132],{"class":102},[89,1745,1746],{"class":106}," MetaData(",[89,1748,395],{"class":886},[89,1750,132],{"class":102},[89,1752,1600],{"class":489},[89,1754,1320],{"class":106},[14,1756,1757,1758,1761,1762,1765],{},"With this in place, running ",[18,1759,1760],{},"alembic revision --autogenerate"," after adding a ",[18,1763,1764],{},"UniqueConstraint"," will emit:",[80,1767,1769],{"className":82,"code":1768,"language":84,"meta":85,"style":85},"op.create_unique_constraint(\n    \"uq_orders_invoice_number\",\n    \"orders\",\n    [\"invoice_number\"],\n)\n",[18,1770,1771,1776,1783,1790,1801],{"__ignoreMap":85},[89,1772,1773],{"class":91,"line":92},[89,1774,1775],{"class":106},"op.create_unique_constraint(\n",[89,1777,1778,1781],{"class":91,"line":99},[89,1779,1780],{"class":485},"    \"uq_orders_invoice_number\"",[89,1782,917],{"class":106},[89,1784,1785,1788],{"class":91,"line":119},[89,1786,1787],{"class":485},"    \"orders\"",[89,1789,917],{"class":106},[89,1791,1792,1795,1798],{"class":91,"line":126},[89,1793,1794],{"class":106},"    [",[89,1796,1797],{"class":485},"\"invoice_number\"",[89,1799,1800],{"class":106},"],\n",[89,1802,1803],{"class":91,"line":199},[89,1804,1320],{"class":106},[14,1806,1807,1808,1811],{},"...rather than the database-assigned ",[18,1809,1810],{},"orders_invoice_number_key",". Deterministic names mean autogenerate can compare the model's expected constraint name against the reflected name and detect true renames versus spurious differences.",[14,1813,1814,1815,1818,1819,1823],{},"Apply the naming convention ",[45,1816,1817],{},"before"," your first migration. Retrofitting it to an existing database requires a migration that drops and recreates every anonymous constraint under its new canonical name — a non-trivial operation covered in the ",[27,1820,1822],{"href":1821},"\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002F","zero-downtime schema migration strategies"," guide.",[461,1825,1827],{"id":1826},"editing-generated-revisions","Editing Generated Revisions",[14,1829,1830,1831,1833],{},"Autogenerate is a starting point, not the final word. Every generated migration should be reviewed and often edited before committing. Open the revision file in ",[18,1832,542],{}," after generation and check it against this list:",[1835,1836,1837,1852,1862,1887],"ol",{},[1838,1839,1840,1843,1844,1847,1848,1851],"li",{},[45,1841,1842],{},"Verify operation order."," ",[18,1845,1846],{},"op.create_table"," must precede ",[18,1849,1850],{},"op.create_foreign_key"," targeting it. Autogenerate generally gets this right, but multi-table cycles require manual reordering.",[1838,1853,1854,1861],{},[45,1855,1856,1857,1860],{},"Remove spurious ",[18,1858,1859],{},"op.drop_table"," directives."," If a third-party table appears in the DB but not in your metadata, autogenerate will propose dropping it. Always check these before committing.",[1838,1863,1864,1871,1872,1875,1876,1879,1880,1883,1884,1130],{},[45,1865,1866,1867,1870],{},"Check ",[18,1868,1869],{},"nullable"," changes."," If a column was ",[18,1873,1874],{},"nullable=True"," in the DB and you declare ",[18,1877,1878],{},"nullable=False"," in the model, autogenerate emits ",[18,1881,1882],{},"op.alter_column(..., nullable=False)"," — which fails if the column has existing NULL values. Add a data-cleanup step before the ",[18,1885,1886],{},"alter_column",[1838,1888,1889,1892],{},[45,1890,1891],{},"Review index naming."," Without a naming convention, index names may differ between the model and the database even when the columns are identical.",[461,1894,1896],{"id":1895},"data-migrations-opbulk_insert-and-chunked-opexecute","Data Migrations: op.bulk_insert and Chunked op.execute",[14,1898,1899,1900,1903,1904,703],{},"For seeding reference data, ",[18,1901,1902],{},"op.bulk_insert"," is cleaner and dialect-portable compared to raw ",[18,1905,1906],{},"op.execute",[80,1908,1910],{"className":82,"code":1909,"language":84,"meta":85,"style":85},"# Seeding reference data in a migration\nfrom alembic import op\nimport sqlalchemy as sa\n\ninvoice_statuses = sa.table(\n    \"invoice_statuses\",\n    sa.column(\"code\", sa.String),\n    sa.column(\"label\", sa.String),\n    sa.column(\"sort_order\", sa.Integer),\n)\n\n\ndef upgrade() -> None:\n    op.create_table(\n        \"invoice_statuses\",\n        sa.Column(\"code\", sa.String(8), primary_key=True),\n        sa.Column(\"label\", sa.String(64), nullable=False),\n        sa.Column(\"sort_order\", sa.Integer, nullable=False, server_default=\"0\"),\n    )\n    op.bulk_insert(\n        invoice_statuses,\n        [\n            {\"code\": \"DRAFT\",  \"label\": \"Draft\",    \"sort_order\": 1},\n            {\"code\": \"SENT\",   \"label\": \"Sent\",      \"sort_order\": 2},\n            {\"code\": \"PAID\",   \"label\": \"Paid\",      \"sort_order\": 3},\n            {\"code\": \"VOID\",   \"label\": \"Voided\",    \"sort_order\": 4},\n            {\"code\": \"REFUND\", \"label\": \"Refunded\",  \"sort_order\": 5},\n        ],\n    )\n\n\ndef downgrade() -> None:\n    op.drop_table(\"invoice_statuses\")\n",[18,1911,1912,1917,1928,1938,1942,1952,1959,1970,1979,1989,1993,1997,2001,2014,2019,2026,2051,2073,2100,2104,2109,2114,2119,2153,2186,2216,2247,2278,2283,2287,2291,2295,2308],{"__ignoreMap":85},[89,1913,1914],{"class":91,"line":92},[89,1915,1916],{"class":95},"# Seeding reference data in a migration\n",[89,1918,1919,1921,1923,1925],{"class":91,"line":99},[89,1920,103],{"class":102},[89,1922,743],{"class":106},[89,1924,110],{"class":102},[89,1926,1927],{"class":106}," op\n",[89,1929,1930,1932,1934,1936],{"class":91,"line":119},[89,1931,110],{"class":102},[89,1933,1354],{"class":106},[89,1935,1043],{"class":102},[89,1937,1359],{"class":106},[89,1939,1940],{"class":91,"line":126},[89,1941,123],{"emptyLinePlaceholder":122},[89,1943,1944,1947,1949],{"class":91,"line":199},[89,1945,1946],{"class":106},"invoice_statuses ",[89,1948,132],{"class":102},[89,1950,1951],{"class":106}," sa.table(\n",[89,1953,1954,1957],{"class":91,"line":209},[89,1955,1956],{"class":485},"    \"invoice_statuses\"",[89,1958,917],{"class":106},[89,1960,1961,1964,1967],{"class":91,"line":219},[89,1962,1963],{"class":106},"    sa.column(",[89,1965,1966],{"class":485},"\"code\"",[89,1968,1969],{"class":106},", sa.String),\n",[89,1971,1972,1974,1977],{"class":91,"line":229},[89,1973,1963],{"class":106},[89,1975,1976],{"class":485},"\"label\"",[89,1978,1969],{"class":106},[89,1980,1981,1983,1986],{"class":91,"line":239},[89,1982,1963],{"class":106},[89,1984,1985],{"class":485},"\"sort_order\"",[89,1987,1988],{"class":106},", sa.Integer),\n",[89,1990,1991],{"class":91,"line":244},[89,1992,1320],{"class":106},[89,1994,1995],{"class":91,"line":794},[89,1996,123],{"emptyLinePlaceholder":122},[89,1998,1999],{"class":91,"line":804},[89,2000,123],{"emptyLinePlaceholder":122},[89,2002,2003,2005,2008,2010,2012],{"class":91,"line":814},[89,2004,868],{"class":102},[89,2006,2007],{"class":481}," upgrade",[89,2009,987],{"class":106},[89,2011,990],{"class":489},[89,2013,993],{"class":106},[89,2015,2016],{"class":91,"line":824},[89,2017,2018],{"class":106},"    op.create_table(\n",[89,2020,2021,2024],{"class":91,"line":829},[89,2022,2023],{"class":485},"        \"invoice_statuses\"",[89,2025,917],{"class":106},[89,2027,2028,2031,2033,2036,2038,2041,2044,2046,2048],{"class":91,"line":840},[89,2029,2030],{"class":106},"        sa.Column(",[89,2032,1966],{"class":485},[89,2034,2035],{"class":106},", sa.String(",[89,2037,286],{"class":489},[89,2039,2040],{"class":106},"), ",[89,2042,2043],{"class":886},"primary_key",[89,2045,132],{"class":102},[89,2047,914],{"class":489},[89,2049,2050],{"class":106},"),\n",[89,2052,2053,2055,2057,2059,2062,2064,2066,2068,2071],{"class":91,"line":846},[89,2054,2030],{"class":106},[89,2056,1976],{"class":485},[89,2058,2035],{"class":106},[89,2060,2061],{"class":489},"64",[89,2063,2040],{"class":106},[89,2065,1869],{"class":886},[89,2067,132],{"class":102},[89,2069,2070],{"class":489},"False",[89,2072,2050],{"class":106},[89,2074,2075,2077,2079,2082,2084,2086,2088,2090,2093,2095,2098],{"class":91,"line":855},[89,2076,2030],{"class":106},[89,2078,1985],{"class":485},[89,2080,2081],{"class":106},", sa.Integer, ",[89,2083,1869],{"class":886},[89,2085,132],{"class":102},[89,2087,2070],{"class":489},[89,2089,64],{"class":106},[89,2091,2092],{"class":886},"server_default",[89,2094,132],{"class":102},[89,2096,2097],{"class":485},"\"0\"",[89,2099,2050],{"class":106},[89,2101,2102],{"class":91,"line":860},[89,2103,947],{"class":106},[89,2105,2106],{"class":91,"line":865},[89,2107,2108],{"class":106},"    op.bulk_insert(\n",[89,2110,2111],{"class":91,"line":877},[89,2112,2113],{"class":106},"        invoice_statuses,\n",[89,2115,2116],{"class":91,"line":883},[89,2117,2118],{"class":106},"        [\n",[89,2120,2121,2124,2126,2128,2131,2133,2135,2137,2140,2143,2145,2147,2150],{"class":91,"line":895},[89,2122,2123],{"class":106},"            {",[89,2125,1966],{"class":485},[89,2127,1614],{"class":106},[89,2129,2130],{"class":485},"\"DRAFT\"",[89,2132,1312],{"class":106},[89,2134,1976],{"class":485},[89,2136,1614],{"class":106},[89,2138,2139],{"class":485},"\"Draft\"",[89,2141,2142],{"class":106},",    ",[89,2144,1985],{"class":485},[89,2146,1614],{"class":106},[89,2148,2149],{"class":489},"1",[89,2151,2152],{"class":106},"},\n",[89,2154,2155,2157,2159,2161,2164,2167,2169,2171,2174,2177,2179,2181,2184],{"class":91,"line":906},[89,2156,2123],{"class":106},[89,2158,1966],{"class":485},[89,2160,1614],{"class":106},[89,2162,2163],{"class":485},"\"SENT\"",[89,2165,2166],{"class":106},",   ",[89,2168,1976],{"class":485},[89,2170,1614],{"class":106},[89,2172,2173],{"class":485},"\"Sent\"",[89,2175,2176],{"class":106},",      ",[89,2178,1985],{"class":485},[89,2180,1614],{"class":106},[89,2182,2183],{"class":489},"2",[89,2185,2152],{"class":106},[89,2187,2188,2190,2192,2194,2197,2199,2201,2203,2206,2208,2210,2212,2214],{"class":91,"line":920},[89,2189,2123],{"class":106},[89,2191,1966],{"class":485},[89,2193,1614],{"class":106},[89,2195,2196],{"class":485},"\"PAID\"",[89,2198,2166],{"class":106},[89,2200,1976],{"class":485},[89,2202,1614],{"class":106},[89,2204,2205],{"class":485},"\"Paid\"",[89,2207,2176],{"class":106},[89,2209,1985],{"class":485},[89,2211,1614],{"class":106},[89,2213,453],{"class":489},[89,2215,2152],{"class":106},[89,2217,2218,2220,2222,2224,2227,2229,2231,2233,2236,2238,2240,2242,2245],{"class":91,"line":932},[89,2219,2123],{"class":106},[89,2221,1966],{"class":485},[89,2223,1614],{"class":106},[89,2225,2226],{"class":485},"\"VOID\"",[89,2228,2166],{"class":106},[89,2230,1976],{"class":485},[89,2232,1614],{"class":106},[89,2234,2235],{"class":485},"\"Voided\"",[89,2237,2142],{"class":106},[89,2239,1985],{"class":485},[89,2241,1614],{"class":106},[89,2243,2244],{"class":489},"4",[89,2246,2152],{"class":106},[89,2248,2249,2251,2253,2255,2258,2260,2262,2264,2267,2269,2271,2273,2276],{"class":91,"line":944},[89,2250,2123],{"class":106},[89,2252,1966],{"class":485},[89,2254,1614],{"class":106},[89,2256,2257],{"class":485},"\"REFUND\"",[89,2259,64],{"class":106},[89,2261,1976],{"class":485},[89,2263,1614],{"class":106},[89,2265,2266],{"class":485},"\"Refunded\"",[89,2268,1312],{"class":106},[89,2270,1985],{"class":485},[89,2272,1614],{"class":106},[89,2274,2275],{"class":489},"5",[89,2277,2152],{"class":106},[89,2279,2280],{"class":91,"line":950},[89,2281,2282],{"class":106},"        ],\n",[89,2284,2285],{"class":91,"line":959},[89,2286,947],{"class":106},[89,2288,2289],{"class":91,"line":965},[89,2290,123],{"emptyLinePlaceholder":122},[89,2292,2293],{"class":91,"line":970},[89,2294,123],{"emptyLinePlaceholder":122},[89,2296,2297,2299,2302,2304,2306],{"class":91,"line":975},[89,2298,868],{"class":102},[89,2300,2301],{"class":481}," downgrade",[89,2303,987],{"class":106},[89,2305,990],{"class":489},[89,2307,993],{"class":106},[89,2309,2310,2313,2316],{"class":91,"line":996},[89,2311,2312],{"class":106},"    op.drop_table(",[89,2314,2315],{"class":485},"\"invoice_statuses\"",[89,2317,1320],{"class":106},[14,2319,2320,2322],{},[18,2321,1902],{}," uses the bound connection directly and does not go through the ORM session, so there is no risk of triggering validators, events, or lazy loads during migration.",[14,2324,2325],{},"For back-filling large tables, batch chunked updates to avoid lock escalation:",[80,2327,2329],{"className":82,"code":2328,"language":84,"meta":85,"style":85},"# upgrade() — chunked back-fill migration for orders.status_code\nfrom alembic import op\nimport sqlalchemy as sa\n\norders_table = sa.table(\n    \"orders\",\n    sa.column(\"id\", sa.Integer),\n    sa.column(\"status\", sa.String),\n    sa.column(\"status_code\", sa.SmallInteger),\n)\n\n\ndef upgrade() -> None:\n    # Step 1: add column as nullable\n    op.add_column(\n        \"orders\",\n        sa.Column(\"status_code\", sa.SmallInteger, nullable=True),\n    )\n\n    # Step 2: back-fill in 10 000-row chunks\n    conn = op.get_bind()\n    while True:\n        result = conn.execute(\n            sa.update(orders_table)\n            .where(orders_table.c.status_code.is_(None))\n            .where(orders_table.c.status.isnot(None))\n            .values(\n                status_code=sa.case(\n                    (orders_table.c.status == \"pending\",   0),\n                    (orders_table.c.status == \"completed\", 1),\n                    (orders_table.c.status == \"cancelled\", 2),\n                    else_=99,\n                )\n            )\n            .returning(orders_table.c.id)\n            .limit(10_000)\n        )\n        if result.rowcount == 0:\n            break\n\n    # Step 3: enforce NOT NULL now that all rows are populated\n    op.alter_column(\"orders\", \"status_code\", nullable=False)\n\n\ndef downgrade() -> None:\n    op.drop_column(\"orders\", \"status_code\")\n",[18,2330,2331,2336,2346,2356,2360,2369,2375,2384,2393,2403,2407,2411,2415,2427,2432,2437,2444,2461,2465,2469,2474,2484,2494,2504,2509,2519,2528,2533,2543,2560,2575,2590,2602,2607,2612,2617,2627,2632,2647,2652,2656,2661,2683,2687,2691,2703],{"__ignoreMap":85},[89,2332,2333],{"class":91,"line":92},[89,2334,2335],{"class":95},"# upgrade() — chunked back-fill migration for orders.status_code\n",[89,2337,2338,2340,2342,2344],{"class":91,"line":99},[89,2339,103],{"class":102},[89,2341,743],{"class":106},[89,2343,110],{"class":102},[89,2345,1927],{"class":106},[89,2347,2348,2350,2352,2354],{"class":91,"line":119},[89,2349,110],{"class":102},[89,2351,1354],{"class":106},[89,2353,1043],{"class":102},[89,2355,1359],{"class":106},[89,2357,2358],{"class":91,"line":126},[89,2359,123],{"emptyLinePlaceholder":122},[89,2361,2362,2365,2367],{"class":91,"line":199},[89,2363,2364],{"class":106},"orders_table ",[89,2366,132],{"class":102},[89,2368,1951],{"class":106},[89,2370,2371,2373],{"class":91,"line":209},[89,2372,1787],{"class":485},[89,2374,917],{"class":106},[89,2376,2377,2379,2382],{"class":91,"line":219},[89,2378,1963],{"class":106},[89,2380,2381],{"class":485},"\"id\"",[89,2383,1988],{"class":106},[89,2385,2386,2388,2391],{"class":91,"line":229},[89,2387,1963],{"class":106},[89,2389,2390],{"class":485},"\"status\"",[89,2392,1969],{"class":106},[89,2394,2395,2397,2400],{"class":91,"line":239},[89,2396,1963],{"class":106},[89,2398,2399],{"class":485},"\"status_code\"",[89,2401,2402],{"class":106},", sa.SmallInteger),\n",[89,2404,2405],{"class":91,"line":244},[89,2406,1320],{"class":106},[89,2408,2409],{"class":91,"line":794},[89,2410,123],{"emptyLinePlaceholder":122},[89,2412,2413],{"class":91,"line":804},[89,2414,123],{"emptyLinePlaceholder":122},[89,2416,2417,2419,2421,2423,2425],{"class":91,"line":814},[89,2418,868],{"class":102},[89,2420,2007],{"class":481},[89,2422,987],{"class":106},[89,2424,990],{"class":489},[89,2426,993],{"class":106},[89,2428,2429],{"class":91,"line":824},[89,2430,2431],{"class":95},"    # Step 1: add column as nullable\n",[89,2433,2434],{"class":91,"line":829},[89,2435,2436],{"class":106},"    op.add_column(\n",[89,2438,2439,2442],{"class":91,"line":840},[89,2440,2441],{"class":485},"        \"orders\"",[89,2443,917],{"class":106},[89,2445,2446,2448,2450,2453,2455,2457,2459],{"class":91,"line":846},[89,2447,2030],{"class":106},[89,2449,2399],{"class":485},[89,2451,2452],{"class":106},", sa.SmallInteger, ",[89,2454,1869],{"class":886},[89,2456,132],{"class":102},[89,2458,914],{"class":489},[89,2460,2050],{"class":106},[89,2462,2463],{"class":91,"line":855},[89,2464,947],{"class":106},[89,2466,2467],{"class":91,"line":860},[89,2468,123],{"emptyLinePlaceholder":122},[89,2470,2471],{"class":91,"line":865},[89,2472,2473],{"class":95},"    # Step 2: back-fill in 10 000-row chunks\n",[89,2475,2476,2479,2481],{"class":91,"line":877},[89,2477,2478],{"class":106},"    conn ",[89,2480,132],{"class":102},[89,2482,2483],{"class":106}," op.get_bind()\n",[89,2485,2486,2489,2492],{"class":91,"line":883},[89,2487,2488],{"class":102},"    while",[89,2490,2491],{"class":489}," True",[89,2493,993],{"class":106},[89,2495,2496,2499,2501],{"class":91,"line":895},[89,2497,2498],{"class":106},"        result ",[89,2500,132],{"class":102},[89,2502,2503],{"class":106}," conn.execute(\n",[89,2505,2506],{"class":91,"line":906},[89,2507,2508],{"class":106},"            sa.update(orders_table)\n",[89,2510,2511,2514,2516],{"class":91,"line":920},[89,2512,2513],{"class":106},"            .where(orders_table.c.status_code.is_(",[89,2515,990],{"class":489},[89,2517,2518],{"class":106},"))\n",[89,2520,2521,2524,2526],{"class":91,"line":932},[89,2522,2523],{"class":106},"            .where(orders_table.c.status.isnot(",[89,2525,990],{"class":489},[89,2527,2518],{"class":106},[89,2529,2530],{"class":91,"line":944},[89,2531,2532],{"class":106},"            .values(\n",[89,2534,2535,2538,2540],{"class":91,"line":950},[89,2536,2537],{"class":886},"                status_code",[89,2539,132],{"class":102},[89,2541,2542],{"class":106},"sa.case(\n",[89,2544,2545,2548,2551,2554,2556,2558],{"class":91,"line":959},[89,2546,2547],{"class":106},"                    (orders_table.c.status ",[89,2549,2550],{"class":102},"==",[89,2552,2553],{"class":485}," \"pending\"",[89,2555,2166],{"class":106},[89,2557,282],{"class":489},[89,2559,2050],{"class":106},[89,2561,2562,2564,2566,2569,2571,2573],{"class":91,"line":965},[89,2563,2547],{"class":106},[89,2565,2550],{"class":102},[89,2567,2568],{"class":485}," \"completed\"",[89,2570,64],{"class":106},[89,2572,2149],{"class":489},[89,2574,2050],{"class":106},[89,2576,2577,2579,2581,2584,2586,2588],{"class":91,"line":970},[89,2578,2547],{"class":106},[89,2580,2550],{"class":102},[89,2582,2583],{"class":485}," \"cancelled\"",[89,2585,64],{"class":106},[89,2587,2183],{"class":489},[89,2589,2050],{"class":106},[89,2591,2592,2595,2597,2600],{"class":91,"line":975},[89,2593,2594],{"class":886},"                    else_",[89,2596,132],{"class":102},[89,2598,2599],{"class":489},"99",[89,2601,917],{"class":106},[89,2603,2604],{"class":91,"line":996},[89,2605,2606],{"class":106},"                )\n",[89,2608,2609],{"class":91,"line":1007},[89,2610,2611],{"class":106},"            )\n",[89,2613,2614],{"class":91,"line":1013},[89,2615,2616],{"class":106},"            .returning(orders_table.c.id)\n",[89,2618,2619,2622,2625],{"class":91,"line":1026},[89,2620,2621],{"class":106},"            .limit(",[89,2623,2624],{"class":489},"10_000",[89,2626,1320],{"class":106},[89,2628,2629],{"class":91,"line":1031},[89,2630,2631],{"class":106},"        )\n",[89,2633,2634,2637,2640,2642,2645],{"class":91,"line":1049},[89,2635,2636],{"class":102},"        if",[89,2638,2639],{"class":106}," result.rowcount ",[89,2641,2550],{"class":102},[89,2643,2644],{"class":489}," 0",[89,2646,993],{"class":106},[89,2648,2649],{"class":91,"line":1058},[89,2650,2651],{"class":102},"            break\n",[89,2653,2654],{"class":91,"line":1067},[89,2655,123],{"emptyLinePlaceholder":122},[89,2657,2658],{"class":91,"line":1072},[89,2659,2660],{"class":95},"    # Step 3: enforce NOT NULL now that all rows are populated\n",[89,2662,2663,2666,2669,2671,2673,2675,2677,2679,2681],{"class":91,"line":1077},[89,2664,2665],{"class":106},"    op.alter_column(",[89,2667,2668],{"class":485},"\"orders\"",[89,2670,64],{"class":106},[89,2672,2399],{"class":485},[89,2674,64],{"class":106},[89,2676,1869],{"class":886},[89,2678,132],{"class":102},[89,2680,2070],{"class":489},[89,2682,1320],{"class":106},[89,2684,2685],{"class":91,"line":1091},[89,2686,123],{"emptyLinePlaceholder":122},[89,2688,2689],{"class":91,"line":1097},[89,2690,123],{"emptyLinePlaceholder":122},[89,2692,2693,2695,2697,2699,2701],{"class":91,"line":1102},[89,2694,868],{"class":102},[89,2696,2301],{"class":481},[89,2698,987],{"class":106},[89,2700,990],{"class":489},[89,2702,993],{"class":106},[89,2704,2705,2708,2710,2712,2714],{"class":91,"line":1107},[89,2706,2707],{"class":106},"    op.drop_column(",[89,2709,2668],{"class":485},[89,2711,64],{"class":106},[89,2713,2399],{"class":485},[89,2715,1320],{"class":106},[14,2717,253,2718,2721,2722,2725,2726,2729,2730,1130],{},[18,2719,2720],{},"RETURNING ... LIMIT 10_000"," pattern on PostgreSQL is efficient — it updates rows, returns their IDs, and the loop exits when no more NULL rows remain. On MySQL (which lacks ",[18,2723,2724],{},"RETURNING","), replace with a ",[18,2727,2728],{},"SELECT ... LIMIT 10_000"," followed by an ",[18,2731,2732],{},"UPDATE ... WHERE id IN (...)",[461,2734,2736],{"id":2735},"branch-and-merge-migrations","Branch and Merge Migrations",[14,2738,2739,2740,2742,2743,703],{},"Alembic's version graph is a DAG, not a linear chain. When two developers each run ",[18,2741,1760],{}," from the same head, they create two branches with the same ",[18,2744,553],{},[80,2746,2750],{"className":2747,"code":2749,"language":297},[2748],"language-text","base\n └─ 001_initial\n     ├─ 002a_add_products  (developer A, down_revision=001)\n     └─ 002b_add_invoices  (developer B, down_revision=001)\n",[18,2751,2749],{"__ignoreMap":85},[14,2753,2754,2755,2758,2759,2762],{},"Running ",[18,2756,2757],{},"alembic upgrade head"," against a branched graph raises ",[18,2760,2761],{},"Multiple head revisions are present for given argument 'head'",". Resolve it with:",[80,2764,2766],{"className":467,"code":2765,"language":469,"meta":85,"style":85},"# Inspect the branch points\nalembic heads\n\n# Merge the two heads into a single revision\nalembic merge -m \"merge_products_and_invoices_branches\" 002a 002b\n",[18,2767,2768,2773,2780,2784,2789],{"__ignoreMap":85},[89,2769,2770],{"class":91,"line":92},[89,2771,2772],{"class":95},"# Inspect the branch points\n",[89,2774,2775,2777],{"class":91,"line":99},[89,2776,482],{"class":481},[89,2778,2779],{"class":485}," heads\n",[89,2781,2782],{"class":91,"line":119},[89,2783,123],{"emptyLinePlaceholder":122},[89,2785,2786],{"class":91,"line":126},[89,2787,2788],{"class":95},"# Merge the two heads into a single revision\n",[89,2790,2791,2793,2796,2798,2801,2804],{"class":91,"line":199},[89,2792,482],{"class":481},[89,2794,2795],{"class":485}," merge",[89,2797,493],{"class":489},[89,2799,2800],{"class":485}," \"merge_products_and_invoices_branches\"",[89,2802,2803],{"class":485}," 002a",[89,2805,2806],{"class":485}," 002b\n",[14,2808,2809,2810,2813,2814,52,2816,2819,2820,2822,2823,2826],{},"This generates a merge revision with ",[18,2811,2812],{},"down_revision = (\"002a...\", \"002b...\")",". The merge revision's ",[18,2815,1241],{},[18,2817,2818],{},"downgrade()"," should be no-ops unless there are real conflicts — for example, both branches adding the same column to the same table. In that case, remove the duplicate ",[18,2821,63],{}," from the merge revision and add a ",[18,2824,2825],{},"try\u002Fexcept"," guard or an existence check.",[14,2828,2829],{},"Avoid branches entirely in single-developer or CI-gated pipelines by always generating revisions from the confirmed head:",[80,2831,2833],{"className":467,"code":2832,"language":469,"meta":85,"style":85},"# Verify before generating\nalembic heads\n\n# Generate from the single confirmed head\nalembic revision --autogenerate -m \"add_tenant_isolation_column\"\n\n# CI gate: fail if there are multiple heads after generation\npython -c \"\nimport subprocess, sys\nresult = subprocess.run(['alembic', 'heads'], capture_output=True, text=True)\nheads = [l for l in result.stdout.splitlines() if l.strip() and '(head)' in l]\nif len(heads) > 1:\n    print(f'ERROR: {len(heads)} heads detected — resolve before merging', file=sys.stderr)\n    sys.exit(1)\n\"\n",[18,2834,2835,2840,2846,2850,2855,2868,2872,2877,2887,2892,2897,2902,2907,2912,2917],{"__ignoreMap":85},[89,2836,2837],{"class":91,"line":92},[89,2838,2839],{"class":95},"# Verify before generating\n",[89,2841,2842,2844],{"class":91,"line":99},[89,2843,482],{"class":481},[89,2845,2779],{"class":485},[89,2847,2848],{"class":91,"line":119},[89,2849,123],{"emptyLinePlaceholder":122},[89,2851,2852],{"class":91,"line":126},[89,2853,2854],{"class":95},"# Generate from the single confirmed head\n",[89,2856,2857,2859,2861,2863,2865],{"class":91,"line":199},[89,2858,482],{"class":481},[89,2860,486],{"class":485},[89,2862,490],{"class":489},[89,2864,493],{"class":489},[89,2866,2867],{"class":485}," \"add_tenant_isolation_column\"\n",[89,2869,2870],{"class":91,"line":209},[89,2871,123],{"emptyLinePlaceholder":122},[89,2873,2874],{"class":91,"line":219},[89,2875,2876],{"class":95},"# CI gate: fail if there are multiple heads after generation\n",[89,2878,2879,2881,2884],{"class":91,"line":229},[89,2880,84],{"class":481},[89,2882,2883],{"class":489}," -c",[89,2885,2886],{"class":485}," \"\n",[89,2888,2889],{"class":91,"line":239},[89,2890,2891],{"class":485},"import subprocess, sys\n",[89,2893,2894],{"class":91,"line":244},[89,2895,2896],{"class":485},"result = subprocess.run(['alembic', 'heads'], capture_output=True, text=True)\n",[89,2898,2899],{"class":91,"line":794},[89,2900,2901],{"class":485},"heads = [l for l in result.stdout.splitlines() if l.strip() and '(head)' in l]\n",[89,2903,2904],{"class":91,"line":804},[89,2905,2906],{"class":485},"if len(heads) > 1:\n",[89,2908,2909],{"class":91,"line":814},[89,2910,2911],{"class":485},"    print(f'ERROR: {len(heads)} heads detected — resolve before merging', file=sys.stderr)\n",[89,2913,2914],{"class":91,"line":824},[89,2915,2916],{"class":485},"    sys.exit(1)\n",[89,2918,2919],{"class":91,"line":829},[89,2920,2921],{"class":485},"\"\n",[37,2923,2925],{"id":2924},"hybrid-architectures-migration-strategies","Hybrid Architectures & Migration Strategies",[14,2927,2928,2929,2931,2932,2934,2935,2937,2938,2941],{},"When mixing SQLAlchemy Core and ORM within the same application, both styles contribute tables to ",[18,2930,338],{}," as long as you define ",[18,2933,51],{}," objects using the same ",[18,2936,24],{}," instance. Core tables defined with ",[18,2939,2940],{},"sa.Table(\"products\", Base.metadata, ...)"," appear in autogenerate output identically to ORM-mapped classes.",[14,2943,2944,2945,2947,2948,2951,2952,2955,2956,2959,2960,2963,2964,2967],{},"For the 1.4 → 2.0 migration path, the most important change in ",[18,2946,77],{}," is replacing ",[18,2949,2950],{},"engine.execute()"," calls with explicit connection contexts — the legacy execution model is removed in 2.0. Also replace ",[18,2953,2954],{},"connection.execute(text(...))"," with ",[18,2957,2958],{},"connection.execute(sa.text(...))"," and ensure every ",[18,2961,2962],{},"text()"," clause uses bound parameters (",[18,2965,2966],{},":param_name"," style) rather than f-string interpolation.",[14,2969,2970,2971,2974,2975,2978],{},"For multi-tenant architectures with one Postgres schema per tenant, use ",[18,2972,2973],{},"version_table_schema"," to pin the ",[18,2976,2977],{},"alembic_version"," table to a shared schema rather than letting it land in each tenant schema:",[80,2980,2982],{"className":82,"code":2981,"language":84,"meta":85,"style":85},"context.configure(\n    connection=connection,\n    target_metadata=target_metadata,\n    include_schemas=True,\n    version_table_schema=\"public\",   # write alembic_version to the public schema\n    version_table=\"alembic_version\",\n)\n",[18,2983,2984,2988,2996,3004,3015,3030,3042],{"__ignoreMap":85},[89,2985,2986],{"class":91,"line":92},[89,2987,1267],{"class":106},[89,2989,2990,2992,2994],{"class":91,"line":99},[89,2991,1272],{"class":886},[89,2993,132],{"class":102},[89,2995,892],{"class":106},[89,2997,2998,3000,3002],{"class":91,"line":119},[89,2999,1281],{"class":886},[89,3001,132],{"class":102},[89,3003,903],{"class":106},[89,3005,3006,3009,3011,3013],{"class":91,"line":126},[89,3007,3008],{"class":886},"    include_schemas",[89,3010,132],{"class":102},[89,3012,914],{"class":489},[89,3014,917],{"class":106},[89,3016,3017,3020,3022,3025,3027],{"class":91,"line":199},[89,3018,3019],{"class":886},"    version_table_schema",[89,3021,132],{"class":102},[89,3023,3024],{"class":485},"\"public\"",[89,3026,2166],{"class":106},[89,3028,3029],{"class":95},"# write alembic_version to the public schema\n",[89,3031,3032,3035,3037,3040],{"class":91,"line":209},[89,3033,3034],{"class":886},"    version_table",[89,3036,132],{"class":102},[89,3038,3039],{"class":485},"\"alembic_version\"",[89,3041,917],{"class":106},[89,3043,3044],{"class":91,"line":219},[89,3045,1320],{"class":106},[14,3047,138,3048,3050,3051,3053],{},[18,3049,2973],{},", Alembic creates an ",[18,3052,2977],{}," table in every schema it encounters, which triggers spurious drop directives on subsequent autogenerate runs against other schemas.",[14,3055,3056,3057,3059,3060,3062,3063,3065,3066,52,3068,3071],{},"For microservice architectures where each service owns a subset of tables in a shared database, the safest pattern is to define a per-service ",[18,3058,24],{}," and use ",[18,3061,1150],{}," to scope autogenerate to only that service's tables. The ",[27,3064,1158],{"href":1157}," provides production-ready ",[18,3067,1150],{},[18,3069,3070],{},"include_name"," implementations for this pattern.",[37,3073,3075],{"id":3074},"production-pitfalls-anti-patterns","Production Pitfalls & Anti-Patterns",[3077,3078,3079,3110,3123,3136,3153,3175],"ul",{},[1838,3080,3081,3086,3087,3090,3091,64,3094,3097,3098,3100,3101,3103,3104,3106,3107,1130],{},[45,3082,3083,3084,1130],{},"Forgetting to import submodules before setting ",[18,3085,34],{}," If ",[18,3088,3089],{},"myapp\u002Fmodels\u002F__init__.py"," does not import ",[18,3092,3093],{},"orders.py",[18,3095,3096],{},"users.py",", etc., those tables are absent from ",[18,3099,338],{},". Autogenerate then emits ",[18,3102,1859],{}," for every table it sees in the database but not in the metadata. Fix: explicitly import all model modules in ",[18,3105,77],{},". Verify with ",[18,3108,3109],{},"python -c \"from myapp.models import Base; print(sorted(Base.metadata.tables.keys()))\"",[1838,3111,3112,3115,3116,3119,3120,3122],{},[45,3113,3114],{},"Running autogenerate against a hand-altered dev database."," If a developer ran ",[18,3117,3118],{},"ALTER TABLE"," manually without going through Alembic, the reflected schema diverges from the migration history. The generated revision is correct against the live database but breaks when applied to a freshly migrated schema. Always autogenerate against a database brought up entirely via ",[18,3121,2757],{}," from a clean state.",[1838,3124,3125,3128,3129,3131,3132,3135],{},[45,3126,3127],{},"Checking in unreviewed autogenerated scripts."," Autogenerate frequently emits ",[18,3130,1859],{}," for tables belonging to PostGIS, Celery, or other applications sharing the same database. These drop directives will silently delete production data. Every generated script must be reviewed with ",[18,3133,3134],{},"git diff"," and manually executed against a staging database before committing.",[1838,3137,3138,3148,3149,3152],{},[45,3139,3140,3141,2955,3143,3145,3146,1130],{},"Using ",[18,3142,387],{},[18,3144,1332],{}," subclasses without stable ",[18,3147,1336],{}," If your custom type does not have a deterministic string representation, type comparison treats every column using it as changed on every autogenerate run. Add ",[18,3150,3151],{},"def __repr__(self): return \"MyType()\""," and run autogenerate twice — the second run must produce no changes.",[1838,3154,3155,3161,3162,3165,3166,2955,3168,3171,3172,3174],{},[45,3156,3157,3158,3160],{},"Raw SQL strings in ",[18,3159,1906],{}," for portable DDL."," A migration using ",[18,3163,3164],{},"op.execute(\"ALTER TABLE orders ADD COLUMN amount NUMERIC DEFAULT 0\")"," is not dialect-portable and breaks on MySQL. Use ",[18,3167,63],{},[18,3169,3170],{},"sa.Column"," wherever Alembic has an abstraction, and reserve ",[18,3173,1906],{}," for DDL that has no Alembic equivalent (triggers, stored procedures, custom Postgres types).",[1838,3176,3177,3183,3184,3186,3187,3190,3191,3194,3195,3198,3199,52,3202,3205],{},[45,3178,3179,3180,3182],{},"Generating a revision while ",[18,3181,2977],{}," is behind the physical schema."," The diff engine reflects the current physical schema, not the schema implied by ",[18,3185,2977],{},". If someone ran a manual ",[18,3188,3189],{},"CREATE INDEX"," or ",[18,3192,3193],{},"ADD COLUMN"," in production, the physical schema is ahead. The next autogenerate run includes those changes in the migration, causing ",[18,3196,3197],{},"DuplicateObject"," errors when applied to databases that were migrated properly. Run ",[18,3200,3201],{},"alembic current",[18,3203,3204],{},"alembic history"," before generating, and audit any discrepancies.",[37,3207,3209],{"id":3208},"frequently-asked-questions","Frequently Asked Questions",[14,3211,3212,3215,3216,3218,3219,3222,3223,3225,3226,3229,3230,3232],{},[45,3213,3214],{},"Why does autogenerate emit an empty migration even though I added a model column?","\nThe model module was not imported before ",[18,3217,34],{}," was assigned. Add ",[18,3220,3221],{},"import myapp.models.orders"," to ",[18,3224,77],{}," before the ",[18,3227,3228],{},"target_metadata = Base.metadata"," line. Verify with ",[18,3231,3109],{}," that all expected tables appear. If the table is present but the column is missing, check that the column is defined at the class level and not only at the instance level.",[14,3234,3235,3241,3242,3245,3246,3248,3249,3251],{},[45,3236,3237,3238,3240],{},"Can I use autogenerate with multiple ",[18,3239,24],{}," objects?","\nYes. Pass a list: ",[18,3243,3244],{},"target_metadata = [AppBase.metadata, AdminBase.metadata]",". Alembic 1.7+ supports this. Each ",[18,3247,24],{}," must use the same ",[18,3250,395],{},", otherwise constraint detection will be unreliable. The diff engine processes each metadata object independently and merges the resulting op lists.",[14,3253,3254,3260,3261,3263,3264,3267,3268,3271,3272,3274,3275,3277],{},[45,3255,3256,3257,3259],{},"Does autogenerate detect changes inside ",[18,3258,2092],{}," expressions?","\nOnly when ",[18,3262,649],{}," is set. Even then, dialect-specific rendering differences cause false positives. For PostgreSQL, ",[18,3265,3266],{},"text(\"now()\")"," in the model and ",[18,3269,3270],{},"CURRENT_TIMESTAMP"," in the reflected schema will not match even though they are semantically equivalent. Write a ",[18,3273,1539],{}," hook to normalize both sides, or use ",[18,3276,1150],{}," to exclude known-unstable columns from server-default comparison.",[14,3279,3280,3287,3288,3290,3291,3293,3294,3297,3298,3300,3301,3303],{},[45,3281,3282,3283,3286],{},"How do I make autogenerate ignore the ",[18,3284,3285],{},"spatial_ref_sys"," table from PostGIS?","\nUse an ",[18,3289,1150],{}," hook in ",[18,3292,77],{},". Pass it to ",[18,3295,3296],{},"context.configure"," alongside ",[18,3299,34],{},". The ",[27,3302,1158],{"href":1157}," shows a complete implementation for PostGIS, Celery, and other third-party tables.",[14,3305,3306,3309,3310,3312,3313,3316,3317,3320],{},[45,3307,3308],{},"What happens if two branches both add the same column to the same table?","\nAlembic's merge revision includes both ",[18,3311,63],{}," calls. At upgrade time, whichever branch applies second attempts to add a column that already exists, causing ",[18,3314,3315],{},"DuplicateColumnError"," (PostgreSQL) or ",[18,3318,3319],{},"OperationalError: duplicate column name"," (MySQL\u002FSQLite). Resolve by manually editing the merge revision to remove the duplicate operation and add a guard comment explaining the resolution.",[37,3322,3324],{"id":3323},"related","Related",[3077,3326,3327,3332,3345,3356],{},[1838,3328,3329,3331],{},[27,3330,30],{"href":29}," — parent pillar covering the full migration lifecycle from project setup to production deployment.",[1838,3333,3334,3337,3338,3340,3341,3344],{},[27,3335,3336],{"href":691},"Configuring Alembic with async SQLAlchemy engines"," — prerequisite: wiring ",[18,3339,77],{}," for ",[18,3342,3343],{},"asyncpg"," before autogenerate can connect to the database.",[1838,3346,3347,3350,3351,52,3353,3355],{},[27,3348,3349],{"href":1157},"Excluding tables and schemas from Alembic autogenerate"," — ",[18,3352,1150],{},[18,3354,3070],{}," hooks to suppress spurious drop directives for third-party tables.",[1838,3357,3358,3361],{},[27,3359,3360],{"href":1821},"Zero-downtime schema migration strategies"," — sequencing migrations on live systems, applying naming conventions retroactively, and safe column alterations.",[3363,3364,3365],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .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 .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":85,"searchDepth":99,"depth":99,"links":3367},[3368,3372,3373,3376,3383,3384,3385,3386],{"id":39,"depth":99,"text":40,"children":3369},[3370,3371],{"id":463,"depth":119,"text":464},{"id":557,"depth":119,"text":558},{"id":684,"depth":99,"text":685},{"id":1140,"depth":99,"text":1141,"children":3374},[3375],{"id":1224,"depth":119,"text":1225},{"id":1245,"depth":99,"text":1246,"children":3377},[3378,3379,3380,3381,3382],{"id":1249,"depth":119,"text":1250},{"id":1545,"depth":119,"text":1546},{"id":1826,"depth":119,"text":1827},{"id":1895,"depth":119,"text":1896},{"id":2735,"depth":119,"text":2736},{"id":2924,"depth":99,"text":2925},{"id":3074,"depth":99,"text":3075},{"id":3208,"depth":99,"text":3209},{"id":3323,"depth":99,"text":3324},"Alembic's revision --autogenerate command compares your SQLAlchemy MetaData object against the live database schema and emits a migration script containing the op-directives needed to bring them in sync — part of the Alembic async migrations and schema evolution workflow every production team needs to master. This guide covers the full cycle: wiring target_metadata, tuning the diff engine, editing generated revisions, writing data migrations, and managing branch graphs.","md",{"date":3390},"2026-06-18","\u002Falembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts",{"title":5,"description":3387},"alembic-async-migrations-and-schema-evolution\u002Fautogenerating-and-reviewing-migration-scripts\u002Findex","j9JQM8F1fDFEmm2_kCIS4GxD-A8W8Z9Gp_2Z-hfBFtY",1781810028981]