[{"data":1,"prerenderedAt":3016},["ShallowReactive",2],{"page-\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Fswitching-schemas-per-request-with-schema-translate-map\u002F":3},{"id":4,"title":5,"body":6,"description":3008,"extension":3009,"meta":3010,"navigation":162,"path":3012,"seo":3013,"stem":3014,"__hash__":3015},"content\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Fswitching-schemas-per-request-with-schema-translate-map\u002Findex.md","Switching Schemas per Request with schema_translate_map",{"type":7,"value":8,"toc":2988},"minimark",[9,13,32,37,40,106,270,294,298,303,330,347,354,358,365,524,528,531,1010,1014,1029,1236,1240,1243,1339,1346,1350,1353,1615,1626,1630,1796,1800,1804,1811,2086,2097,2101,2107,2315,2322,2326,2340,2487,2491,2498,2534,2726,2733,2861,2865,2898,2911,2929,2951,2955,2984],[10,11,5],"h1",{"id":12},"switching-schemas-per-request-with-schema_translate_map",[14,15,16,17,21,22,25,26,31],"p",{},"Pass ",[18,19,20],"code",{},"execution_options={\"schema_translate_map\": {None: tenant_schema}}"," to ",[18,23,24],{},"session.execute()"," on every statement to rewrite all unqualified table references to the target PostgreSQL schema at SQLAlchemy compile time — no model changes required. This is the recommended pattern in the ",[27,28,30],"a",{"href":29},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002F","Dynamic Schema and Multi-Tenant Routing"," architecture for SaaS applications that isolate tenants into separate schemas within a shared database.",[33,34,36],"h2",{"id":35},"quick-answer","Quick Answer",[14,38,39],{},"The before\u002Fafter contrast shows the 1.4 workaround (manual string formatting) versus the clean 2.0 approach:",[41,42,47],"pre",{"className":43,"code":44,"language":45,"meta":46,"style":46},"language-python shiki shiki-themes github-light github-dark","# Legacy pattern — do NOT use (SQL injection risk, breaks compiled caching)\nschema = \"acme\"\nraw_sql = f\"SELECT * FROM {schema}.invoices WHERE paid = false\"\nsession.execute(text(raw_sql))\n","python","",[18,48,49,58,73,100],{"__ignoreMap":46},[50,51,54],"span",{"class":52,"line":53},"line",1,[50,55,57],{"class":56},"sJ8bj","# Legacy pattern — do NOT use (SQL injection risk, breaks compiled caching)\n",[50,59,61,65,69],{"class":52,"line":60},2,[50,62,64],{"class":63},"sVt8B","schema ",[50,66,68],{"class":67},"szBVR","=",[50,70,72],{"class":71},"sZZnC"," \"acme\"\n",[50,74,76,79,81,84,87,91,94,97],{"class":52,"line":75},3,[50,77,78],{"class":63},"raw_sql ",[50,80,68],{"class":67},[50,82,83],{"class":67}," f",[50,85,86],{"class":71},"\"SELECT * FROM ",[50,88,90],{"class":89},"sj4cs","{",[50,92,93],{"class":63},"schema",[50,95,96],{"class":89},"}",[50,98,99],{"class":71},".invoices WHERE paid = false\"\n",[50,101,103],{"class":52,"line":102},4,[50,104,105],{"class":63},"session.execute(text(raw_sql))\n",[41,107,109],{"className":43,"code":108,"language":45,"meta":46,"style":46},"# SQLAlchemy 2.0 — schema_translate_map rewrites compiled SQL safely\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom your_models import Invoice  # Invoice.__table_args__ = {\"schema\": None}\n\nasync def get_unpaid_invoices(session: AsyncSession, tenant_schema: str) -> list[Invoice]:\n    stmt = select(Invoice).where(Invoice.paid == False)\n    result = await session.execute(\n        stmt,\n        execution_options={\"schema_translate_map\": {None: tenant_schema}},\n    )\n    return result.scalars().all()\n    # Emits: SELECT invoices.id, ... FROM acme.invoices WHERE invoices.paid = false\n",[18,110,111,116,130,142,157,164,186,206,220,226,249,255,264],{"__ignoreMap":46},[50,112,113],{"class":52,"line":53},[50,114,115],{"class":56},"# SQLAlchemy 2.0 — schema_translate_map rewrites compiled SQL safely\n",[50,117,118,121,124,127],{"class":52,"line":60},[50,119,120],{"class":67},"from",[50,122,123],{"class":63}," sqlalchemy ",[50,125,126],{"class":67},"import",[50,128,129],{"class":63}," select\n",[50,131,132,134,137,139],{"class":52,"line":75},[50,133,120],{"class":67},[50,135,136],{"class":63}," sqlalchemy.ext.asyncio ",[50,138,126],{"class":67},[50,140,141],{"class":63}," AsyncSession\n",[50,143,144,146,149,151,154],{"class":52,"line":102},[50,145,120],{"class":67},[50,147,148],{"class":63}," your_models ",[50,150,126],{"class":67},[50,152,153],{"class":63}," Invoice  ",[50,155,156],{"class":56},"# Invoice.__table_args__ = {\"schema\": None}\n",[50,158,160],{"class":52,"line":159},5,[50,161,163],{"emptyLinePlaceholder":162},true,"\n",[50,165,167,170,173,177,180,183],{"class":52,"line":166},6,[50,168,169],{"class":67},"async",[50,171,172],{"class":67}," def",[50,174,176],{"class":175},"sScJk"," get_unpaid_invoices",[50,178,179],{"class":63},"(session: AsyncSession, tenant_schema: ",[50,181,182],{"class":89},"str",[50,184,185],{"class":63},") -> list[Invoice]:\n",[50,187,189,192,194,197,200,203],{"class":52,"line":188},7,[50,190,191],{"class":63},"    stmt ",[50,193,68],{"class":67},[50,195,196],{"class":63}," select(Invoice).where(Invoice.paid ",[50,198,199],{"class":67},"==",[50,201,202],{"class":89}," False",[50,204,205],{"class":63},")\n",[50,207,209,212,214,217],{"class":52,"line":208},8,[50,210,211],{"class":63},"    result ",[50,213,68],{"class":67},[50,215,216],{"class":67}," await",[50,218,219],{"class":63}," session.execute(\n",[50,221,223],{"class":52,"line":222},9,[50,224,225],{"class":63},"        stmt,\n",[50,227,229,233,235,237,240,243,246],{"class":52,"line":228},10,[50,230,232],{"class":231},"s4XuR","        execution_options",[50,234,68],{"class":67},[50,236,90],{"class":63},[50,238,239],{"class":71},"\"schema_translate_map\"",[50,241,242],{"class":63},": {",[50,244,245],{"class":89},"None",[50,247,248],{"class":63},": tenant_schema}},\n",[50,250,252],{"class":52,"line":251},11,[50,253,254],{"class":63},"    )\n",[50,256,258,261],{"class":52,"line":257},12,[50,259,260],{"class":67},"    return",[50,262,263],{"class":63}," result.scalars().all()\n",[50,265,267],{"class":52,"line":266},13,[50,268,269],{"class":56},"    # Emits: SELECT invoices.id, ... FROM acme.invoices WHERE invoices.paid = false\n",[14,271,272,273,275,276,278,279,282,283,286,287,290,291,293],{},"The map key (",[18,274,245],{},") matches the ",[18,277,93],{}," value declared in the model's ",[18,280,281],{},"__table_args__",". If your model declares ",[18,284,285],{},"schema=\"shared\"",", the key must be ",[18,288,289],{},"\"shared\"",", not ",[18,292,245],{},".",[33,295,297],{"id":296},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[299,300,302],"h3",{"id":301},"why-schema_translate_map-is-connection-pool-safe","Why schema_translate_map is Connection-Pool-Safe",[14,304,305,306,309,310,313,314,317,318,321,322,325,326,329],{},"SQLAlchemy compiles each ",[18,307,308],{},"select()"," or ",[18,311,312],{},"insert()"," statement into a ",[18,315,316],{},"ClauseElement"," object and caches the compiled SQL string. When ",[18,319,320],{},"schema_translate_map"," is applied, it acts as a compilation modifier: the cache key includes the map, so ",[18,323,324],{},"{None: \"acme\"}"," and ",[18,327,328],{},"{None: \"globex\"}"," produce two distinct cached SQL strings. This means:",[331,332,333,337,340],"ol",{},[334,335,336],"li",{},"The rewriting happens entirely in Python, before the SQL reaches the DBAPI.",[334,338,339],{},"No connection-level session variable is set on the database server.",[334,341,342,343,346],{},"The underlying ",[18,344,345],{},"asyncpg"," connection is returned to the pool in a clean state after the query, with no tenant-specific residue.",[14,348,349,350,353],{},"This is fundamentally different from ",[18,351,352],{},"SET search_path = acme"," (a server-side session variable), which persists on the connection until explicitly reset — a serious problem with PgBouncer or any connection pool that reuses connections across tenants.",[299,355,357],{"id":356},"async-session-factory-setup","Async Session Factory Setup",[14,359,360,361,364],{},"The ",[18,362,363],{},"async_sessionmaker"," factory is shared globally. Schema context is injected per statement, not per session:",[41,366,368],{"className":43,"code":367,"language":45,"meta":46,"style":46},"from sqlalchemy.ext.asyncio import (\n    AsyncSession,\n    create_async_engine,\n    async_sessionmaker,\n)\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fsaas_db\",\n    pool_size=20,\n    max_overflow=10,\n    pool_pre_ping=True,\n)\n\n# expire_on_commit=False is critical in async FastAPI: after commit the response\n# serializer still needs to read attributes, and the session may be closed.\nAsyncSessionLocal = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,\n)\n",[18,369,370,381,386,391,396,400,404,414,422,434,446,458,462,466,472,478,489,495,506,519],{"__ignoreMap":46},[50,371,372,374,376,378],{"class":52,"line":53},[50,373,120],{"class":67},[50,375,136],{"class":63},[50,377,126],{"class":67},[50,379,380],{"class":63}," (\n",[50,382,383],{"class":52,"line":60},[50,384,385],{"class":63},"    AsyncSession,\n",[50,387,388],{"class":52,"line":75},[50,389,390],{"class":63},"    create_async_engine,\n",[50,392,393],{"class":52,"line":102},[50,394,395],{"class":63},"    async_sessionmaker,\n",[50,397,398],{"class":52,"line":159},[50,399,205],{"class":63},[50,401,402],{"class":52,"line":166},[50,403,163],{"emptyLinePlaceholder":162},[50,405,406,409,411],{"class":52,"line":188},[50,407,408],{"class":63},"engine ",[50,410,68],{"class":67},[50,412,413],{"class":63}," create_async_engine(\n",[50,415,416,419],{"class":52,"line":208},[50,417,418],{"class":71},"    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fsaas_db\"",[50,420,421],{"class":63},",\n",[50,423,424,427,429,432],{"class":52,"line":222},[50,425,426],{"class":231},"    pool_size",[50,428,68],{"class":67},[50,430,431],{"class":89},"20",[50,433,421],{"class":63},[50,435,436,439,441,444],{"class":52,"line":228},[50,437,438],{"class":231},"    max_overflow",[50,440,68],{"class":67},[50,442,443],{"class":89},"10",[50,445,421],{"class":63},[50,447,448,451,453,456],{"class":52,"line":251},[50,449,450],{"class":231},"    pool_pre_ping",[50,452,68],{"class":67},[50,454,455],{"class":89},"True",[50,457,421],{"class":63},[50,459,460],{"class":52,"line":257},[50,461,205],{"class":63},[50,463,464],{"class":52,"line":266},[50,465,163],{"emptyLinePlaceholder":162},[50,467,469],{"class":52,"line":468},14,[50,470,471],{"class":56},"# expire_on_commit=False is critical in async FastAPI: after commit the response\n",[50,473,475],{"class":52,"line":474},15,[50,476,477],{"class":56},"# serializer still needs to read attributes, and the session may be closed.\n",[50,479,481,484,486],{"class":52,"line":480},16,[50,482,483],{"class":63},"AsyncSessionLocal ",[50,485,68],{"class":67},[50,487,488],{"class":63}," async_sessionmaker(\n",[50,490,492],{"class":52,"line":491},17,[50,493,494],{"class":63},"    engine,\n",[50,496,498,501,503],{"class":52,"line":497},18,[50,499,500],{"class":231},"    class_",[50,502,68],{"class":67},[50,504,505],{"class":63},"AsyncSession,\n",[50,507,509,512,514,517],{"class":52,"line":508},19,[50,510,511],{"class":231},"    expire_on_commit",[50,513,68],{"class":67},[50,515,516],{"class":89},"False",[50,518,421],{"class":63},[50,520,522],{"class":52,"line":521},20,[50,523,205],{"class":63},[299,525,527],{"id":526},"fastapi-dependency-wiring","FastAPI Dependency Wiring",[14,529,530],{},"The tenant schema should be derived from a verified claim — a JWT sub-tenant field, a validated subdomain, or a server-side lookup — never passed raw from a request header.",[41,532,534],{"className":43,"code":533,"language":45,"meta":46,"style":46},"from fastapi import Depends, FastAPI, Header, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom typing import AsyncGenerator, Annotated\n\napp = FastAPI()\n\n# Allowlist of valid schema names — populated from your tenants table at startup\nVALID_SCHEMAS: dict[str, str] = {\n    \"acme\": \"acme\",\n    \"globex\": \"globex\",\n    \"initech\": \"initech\",\n}\n\n\nasync def get_db() -> AsyncGenerator[AsyncSession, None]:\n    async with AsyncSessionLocal() as session:\n        yield session\n\n\ndef resolve_tenant_schema(\n    x_tenant_id: Annotated[str, Header()],\n) -> str:\n    schema = VALID_SCHEMAS.get(x_tenant_id)\n    if schema is None:\n        raise HTTPException(status_code=403, detail=\"Unknown tenant\")\n    return schema\n\n\n@app.get(\"\u002Finvoices\")\nasync def list_invoices(\n    schema: str = Depends(resolve_tenant_schema),\n    session: AsyncSession = Depends(get_db),\n):\n    from your_models import Invoice\n\n    stmt = select(Invoice).where(Invoice.paid == False).order_by(Invoice.id)\n    result = await session.execute(\n        stmt,\n        execution_options={\"schema_translate_map\": {None: schema}},\n    )\n    invoices = result.scalars().all()\n    return [{\"id\": inv.id, \"amount\": inv.amount_cents} for inv in invoices]\n",[18,535,536,548,558,568,580,584,594,598,603,626,639,651,663,668,672,676,693,710,718,722,726,738,749,760,774,791,820,828,833,838,852,864,878,889,895,908,913,929,940,945,963,968,978],{"__ignoreMap":46},[50,537,538,540,543,545],{"class":52,"line":53},[50,539,120],{"class":67},[50,541,542],{"class":63}," fastapi ",[50,544,126],{"class":67},[50,546,547],{"class":63}," Depends, FastAPI, Header, HTTPException\n",[50,549,550,552,554,556],{"class":52,"line":60},[50,551,120],{"class":67},[50,553,123],{"class":63},[50,555,126],{"class":67},[50,557,129],{"class":63},[50,559,560,562,564,566],{"class":52,"line":75},[50,561,120],{"class":67},[50,563,136],{"class":63},[50,565,126],{"class":67},[50,567,141],{"class":63},[50,569,570,572,575,577],{"class":52,"line":102},[50,571,120],{"class":67},[50,573,574],{"class":63}," typing ",[50,576,126],{"class":67},[50,578,579],{"class":63}," AsyncGenerator, Annotated\n",[50,581,582],{"class":52,"line":159},[50,583,163],{"emptyLinePlaceholder":162},[50,585,586,589,591],{"class":52,"line":166},[50,587,588],{"class":63},"app ",[50,590,68],{"class":67},[50,592,593],{"class":63}," FastAPI()\n",[50,595,596],{"class":52,"line":188},[50,597,163],{"emptyLinePlaceholder":162},[50,599,600],{"class":52,"line":208},[50,601,602],{"class":56},"# Allowlist of valid schema names — populated from your tenants table at startup\n",[50,604,605,608,611,613,616,618,621,623],{"class":52,"line":222},[50,606,607],{"class":89},"VALID_SCHEMAS",[50,609,610],{"class":63},": dict[",[50,612,182],{"class":89},[50,614,615],{"class":63},", ",[50,617,182],{"class":89},[50,619,620],{"class":63},"] ",[50,622,68],{"class":67},[50,624,625],{"class":63}," {\n",[50,627,628,631,634,637],{"class":52,"line":228},[50,629,630],{"class":71},"    \"acme\"",[50,632,633],{"class":63},": ",[50,635,636],{"class":71},"\"acme\"",[50,638,421],{"class":63},[50,640,641,644,646,649],{"class":52,"line":251},[50,642,643],{"class":71},"    \"globex\"",[50,645,633],{"class":63},[50,647,648],{"class":71},"\"globex\"",[50,650,421],{"class":63},[50,652,653,656,658,661],{"class":52,"line":257},[50,654,655],{"class":71},"    \"initech\"",[50,657,633],{"class":63},[50,659,660],{"class":71},"\"initech\"",[50,662,421],{"class":63},[50,664,665],{"class":52,"line":266},[50,666,667],{"class":63},"}\n",[50,669,670],{"class":52,"line":468},[50,671,163],{"emptyLinePlaceholder":162},[50,673,674],{"class":52,"line":474},[50,675,163],{"emptyLinePlaceholder":162},[50,677,678,680,682,685,688,690],{"class":52,"line":480},[50,679,169],{"class":67},[50,681,172],{"class":67},[50,683,684],{"class":175}," get_db",[50,686,687],{"class":63},"() -> AsyncGenerator[AsyncSession, ",[50,689,245],{"class":89},[50,691,692],{"class":63},"]:\n",[50,694,695,698,701,704,707],{"class":52,"line":491},[50,696,697],{"class":67},"    async",[50,699,700],{"class":67}," with",[50,702,703],{"class":63}," AsyncSessionLocal() ",[50,705,706],{"class":67},"as",[50,708,709],{"class":63}," session:\n",[50,711,712,715],{"class":52,"line":497},[50,713,714],{"class":67},"        yield",[50,716,717],{"class":63}," session\n",[50,719,720],{"class":52,"line":508},[50,721,163],{"emptyLinePlaceholder":162},[50,723,724],{"class":52,"line":521},[50,725,163],{"emptyLinePlaceholder":162},[50,727,729,732,735],{"class":52,"line":728},21,[50,730,731],{"class":67},"def",[50,733,734],{"class":175}," resolve_tenant_schema",[50,736,737],{"class":63},"(\n",[50,739,741,744,746],{"class":52,"line":740},22,[50,742,743],{"class":63},"    x_tenant_id: Annotated[",[50,745,182],{"class":89},[50,747,748],{"class":63},", Header()],\n",[50,750,752,755,757],{"class":52,"line":751},23,[50,753,754],{"class":63},") -> ",[50,756,182],{"class":89},[50,758,759],{"class":63},":\n",[50,761,763,766,768,771],{"class":52,"line":762},24,[50,764,765],{"class":63},"    schema ",[50,767,68],{"class":67},[50,769,770],{"class":89}," VALID_SCHEMAS",[50,772,773],{"class":63},".get(x_tenant_id)\n",[50,775,777,780,783,786,789],{"class":52,"line":776},25,[50,778,779],{"class":67},"    if",[50,781,782],{"class":63}," schema ",[50,784,785],{"class":67},"is",[50,787,788],{"class":89}," None",[50,790,759],{"class":63},[50,792,794,797,800,803,805,808,810,813,815,818],{"class":52,"line":793},26,[50,795,796],{"class":67},"        raise",[50,798,799],{"class":63}," HTTPException(",[50,801,802],{"class":231},"status_code",[50,804,68],{"class":67},[50,806,807],{"class":89},"403",[50,809,615],{"class":63},[50,811,812],{"class":231},"detail",[50,814,68],{"class":67},[50,816,817],{"class":71},"\"Unknown tenant\"",[50,819,205],{"class":63},[50,821,823,825],{"class":52,"line":822},27,[50,824,260],{"class":67},[50,826,827],{"class":63}," schema\n",[50,829,831],{"class":52,"line":830},28,[50,832,163],{"emptyLinePlaceholder":162},[50,834,836],{"class":52,"line":835},29,[50,837,163],{"emptyLinePlaceholder":162},[50,839,841,844,847,850],{"class":52,"line":840},30,[50,842,843],{"class":175},"@app.get",[50,845,846],{"class":63},"(",[50,848,849],{"class":71},"\"\u002Finvoices\"",[50,851,205],{"class":63},[50,853,855,857,859,862],{"class":52,"line":854},31,[50,856,169],{"class":67},[50,858,172],{"class":67},[50,860,861],{"class":175}," list_invoices",[50,863,737],{"class":63},[50,865,867,870,872,875],{"class":52,"line":866},32,[50,868,869],{"class":63},"    schema: ",[50,871,182],{"class":89},[50,873,874],{"class":67}," =",[50,876,877],{"class":63}," Depends(resolve_tenant_schema),\n",[50,879,881,884,886],{"class":52,"line":880},33,[50,882,883],{"class":63},"    session: AsyncSession ",[50,885,68],{"class":67},[50,887,888],{"class":63}," Depends(get_db),\n",[50,890,892],{"class":52,"line":891},34,[50,893,894],{"class":63},"):\n",[50,896,898,901,903,905],{"class":52,"line":897},35,[50,899,900],{"class":67},"    from",[50,902,148],{"class":63},[50,904,126],{"class":67},[50,906,907],{"class":63}," Invoice\n",[50,909,911],{"class":52,"line":910},36,[50,912,163],{"emptyLinePlaceholder":162},[50,914,916,918,920,922,924,926],{"class":52,"line":915},37,[50,917,191],{"class":63},[50,919,68],{"class":67},[50,921,196],{"class":63},[50,923,199],{"class":67},[50,925,202],{"class":89},[50,927,928],{"class":63},").order_by(Invoice.id)\n",[50,930,932,934,936,938],{"class":52,"line":931},38,[50,933,211],{"class":63},[50,935,68],{"class":67},[50,937,216],{"class":67},[50,939,219],{"class":63},[50,941,943],{"class":52,"line":942},39,[50,944,225],{"class":63},[50,946,948,950,952,954,956,958,960],{"class":52,"line":947},40,[50,949,232],{"class":231},[50,951,68],{"class":67},[50,953,90],{"class":63},[50,955,239],{"class":71},[50,957,242],{"class":63},[50,959,245],{"class":89},[50,961,962],{"class":63},": schema}},\n",[50,964,966],{"class":52,"line":965},41,[50,967,254],{"class":63},[50,969,971,974,976],{"class":52,"line":970},42,[50,972,973],{"class":63},"    invoices ",[50,975,68],{"class":67},[50,977,263],{"class":63},[50,979,981,983,986,989,992,995,998,1001,1004,1007],{"class":52,"line":980},43,[50,982,260],{"class":67},[50,984,985],{"class":63}," [{",[50,987,988],{"class":71},"\"id\"",[50,990,991],{"class":63},": inv.id, ",[50,993,994],{"class":71},"\"amount\"",[50,996,997],{"class":63},": inv.amount_cents} ",[50,999,1000],{"class":67},"for",[50,1002,1003],{"class":63}," inv ",[50,1005,1006],{"class":67},"in",[50,1008,1009],{"class":63}," invoices]\n",[299,1011,1013],{"id":1012},"applying-the-map-to-write-operations","Applying the Map to Write Operations",[14,1015,1016,1018,1019,615,1021,1024,1025,1028],{},[18,1017,320],{}," works identically for ",[18,1020,312],{},[18,1022,1023],{},"update()",", and ",[18,1026,1027],{},"delete()"," statements compiled through SQLAlchemy Core:",[41,1030,1032],{"className":43,"code":1031,"language":45,"meta":46,"style":46},"from sqlalchemy import insert, update\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom your_models import Invoice\nimport datetime\n\n\nasync def create_invoice(\n    session: AsyncSession,\n    tenant_schema: str,\n    amount_cents: int,\n) -> int:\n    stmt = (\n        insert(Invoice)\n        .values(tenant_id=1, amount_cents=amount_cents, paid=False)\n        .returning(Invoice.id)\n    )\n    result = await session.execute(\n        stmt,\n        execution_options={\"schema_translate_map\": {None: tenant_schema}},\n    )\n    await session.commit()\n    return result.scalar_one()\n    # Emits: INSERT INTO acme.invoices (tenant_id, amount_cents, paid)\n    #        VALUES (1, 5000, false) RETURNING invoices.id\n",[18,1033,1034,1045,1055,1065,1072,1076,1080,1091,1096,1105,1115,1123,1131,1136,1168,1173,1177,1187,1191,1207,1211,1219,1226,1231],{"__ignoreMap":46},[50,1035,1036,1038,1040,1042],{"class":52,"line":53},[50,1037,120],{"class":67},[50,1039,123],{"class":63},[50,1041,126],{"class":67},[50,1043,1044],{"class":63}," insert, update\n",[50,1046,1047,1049,1051,1053],{"class":52,"line":60},[50,1048,120],{"class":67},[50,1050,136],{"class":63},[50,1052,126],{"class":67},[50,1054,141],{"class":63},[50,1056,1057,1059,1061,1063],{"class":52,"line":75},[50,1058,120],{"class":67},[50,1060,148],{"class":63},[50,1062,126],{"class":67},[50,1064,907],{"class":63},[50,1066,1067,1069],{"class":52,"line":102},[50,1068,126],{"class":67},[50,1070,1071],{"class":63}," datetime\n",[50,1073,1074],{"class":52,"line":159},[50,1075,163],{"emptyLinePlaceholder":162},[50,1077,1078],{"class":52,"line":166},[50,1079,163],{"emptyLinePlaceholder":162},[50,1081,1082,1084,1086,1089],{"class":52,"line":188},[50,1083,169],{"class":67},[50,1085,172],{"class":67},[50,1087,1088],{"class":175}," create_invoice",[50,1090,737],{"class":63},[50,1092,1093],{"class":52,"line":208},[50,1094,1095],{"class":63},"    session: AsyncSession,\n",[50,1097,1098,1101,1103],{"class":52,"line":222},[50,1099,1100],{"class":63},"    tenant_schema: ",[50,1102,182],{"class":89},[50,1104,421],{"class":63},[50,1106,1107,1110,1113],{"class":52,"line":228},[50,1108,1109],{"class":63},"    amount_cents: ",[50,1111,1112],{"class":89},"int",[50,1114,421],{"class":63},[50,1116,1117,1119,1121],{"class":52,"line":251},[50,1118,754],{"class":63},[50,1120,1112],{"class":89},[50,1122,759],{"class":63},[50,1124,1125,1127,1129],{"class":52,"line":257},[50,1126,191],{"class":63},[50,1128,68],{"class":67},[50,1130,380],{"class":63},[50,1132,1133],{"class":52,"line":266},[50,1134,1135],{"class":63},"        insert(Invoice)\n",[50,1137,1138,1141,1144,1146,1149,1151,1154,1156,1159,1162,1164,1166],{"class":52,"line":468},[50,1139,1140],{"class":63},"        .values(",[50,1142,1143],{"class":231},"tenant_id",[50,1145,68],{"class":67},[50,1147,1148],{"class":89},"1",[50,1150,615],{"class":63},[50,1152,1153],{"class":231},"amount_cents",[50,1155,68],{"class":67},[50,1157,1158],{"class":63},"amount_cents, ",[50,1160,1161],{"class":231},"paid",[50,1163,68],{"class":67},[50,1165,516],{"class":89},[50,1167,205],{"class":63},[50,1169,1170],{"class":52,"line":474},[50,1171,1172],{"class":63},"        .returning(Invoice.id)\n",[50,1174,1175],{"class":52,"line":480},[50,1176,254],{"class":63},[50,1178,1179,1181,1183,1185],{"class":52,"line":491},[50,1180,211],{"class":63},[50,1182,68],{"class":67},[50,1184,216],{"class":67},[50,1186,219],{"class":63},[50,1188,1189],{"class":52,"line":497},[50,1190,225],{"class":63},[50,1192,1193,1195,1197,1199,1201,1203,1205],{"class":52,"line":508},[50,1194,232],{"class":231},[50,1196,68],{"class":67},[50,1198,90],{"class":63},[50,1200,239],{"class":71},[50,1202,242],{"class":63},[50,1204,245],{"class":89},[50,1206,248],{"class":63},[50,1208,1209],{"class":52,"line":521},[50,1210,254],{"class":63},[50,1212,1213,1216],{"class":52,"line":728},[50,1214,1215],{"class":67},"    await",[50,1217,1218],{"class":63}," session.commit()\n",[50,1220,1221,1223],{"class":52,"line":740},[50,1222,260],{"class":67},[50,1224,1225],{"class":63}," result.scalar_one()\n",[50,1227,1228],{"class":52,"line":751},[50,1229,1230],{"class":56},"    # Emits: INSERT INTO acme.invoices (tenant_id, amount_cents, paid)\n",[50,1232,1233],{"class":52,"line":762},[50,1234,1235],{"class":56},"    #        VALUES (1, 5000, false) RETURNING invoices.id\n",[299,1237,1239],{"id":1238},"models-with-multiple-declared-schemas","Models with Multiple Declared Schemas",[14,1241,1242],{},"When your application mixes unschema'd models (public tables) with per-tenant models, use a multi-entry map:",[41,1244,1246],{"className":43,"code":1245,"language":45,"meta":46,"style":46},"from your_models import Invoice, Product, GlobalConfig\n\n# GlobalConfig lives in the public schema — leave it unmapped\n# Invoice and Product live in None schema — map to tenant\nstmt = select(Invoice, Product).join(Product, Invoice.product_id == Product.id)\nresult = await session.execute(\n    stmt,\n    execution_options={\"schema_translate_map\": {None: \"acme\", \"public\": \"public\"}},\n)\n",[18,1247,1248,1259,1263,1268,1273,1288,1299,1304,1335],{"__ignoreMap":46},[50,1249,1250,1252,1254,1256],{"class":52,"line":53},[50,1251,120],{"class":67},[50,1253,148],{"class":63},[50,1255,126],{"class":67},[50,1257,1258],{"class":63}," Invoice, Product, GlobalConfig\n",[50,1260,1261],{"class":52,"line":60},[50,1262,163],{"emptyLinePlaceholder":162},[50,1264,1265],{"class":52,"line":75},[50,1266,1267],{"class":56},"# GlobalConfig lives in the public schema — leave it unmapped\n",[50,1269,1270],{"class":52,"line":102},[50,1271,1272],{"class":56},"# Invoice and Product live in None schema — map to tenant\n",[50,1274,1275,1278,1280,1283,1285],{"class":52,"line":159},[50,1276,1277],{"class":63},"stmt ",[50,1279,68],{"class":67},[50,1281,1282],{"class":63}," select(Invoice, Product).join(Product, Invoice.product_id ",[50,1284,199],{"class":67},[50,1286,1287],{"class":63}," Product.id)\n",[50,1289,1290,1293,1295,1297],{"class":52,"line":166},[50,1291,1292],{"class":63},"result ",[50,1294,68],{"class":67},[50,1296,216],{"class":67},[50,1298,219],{"class":63},[50,1300,1301],{"class":52,"line":188},[50,1302,1303],{"class":63},"    stmt,\n",[50,1305,1306,1309,1311,1313,1315,1317,1319,1321,1323,1325,1328,1330,1332],{"class":52,"line":208},[50,1307,1308],{"class":231},"    execution_options",[50,1310,68],{"class":67},[50,1312,90],{"class":63},[50,1314,239],{"class":71},[50,1316,242],{"class":63},[50,1318,245],{"class":89},[50,1320,633],{"class":63},[50,1322,636],{"class":71},[50,1324,615],{"class":63},[50,1326,1327],{"class":71},"\"public\"",[50,1329,633],{"class":63},[50,1331,1327],{"class":71},[50,1333,1334],{"class":63},"}},\n",[50,1336,1337],{"class":52,"line":222},[50,1338,205],{"class":63},[14,1340,1341,1342,1345],{},"Including ",[18,1343,1344],{},"\"public\": \"public\""," in the map is a no-op but makes intent explicit and prevents accidental rewriting if models are later moved.",[299,1347,1349],{"id":1348},"using-schema_translate_map-inside-background-tasks","Using schema_translate_map Inside Background Tasks",[14,1351,1352],{},"FastAPI background tasks and Celery workers need the same per-operation schema routing as request handlers. The key difference is that there is no incoming HTTP request to derive the tenant from — the schema name must be passed explicitly as a parameter:",[41,1354,1356],{"className":43,"code":1355,"language":45,"meta":46,"style":46},"from fastapi import BackgroundTasks\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nasync def send_invoice_email_background(session: AsyncSession, schema: str, invoice_id: int):\n    \"\"\"Background task: fetch invoice from tenant schema and send email.\"\"\"\n    from sqlalchemy import select\n    from your_models import Invoice\n\n    stmt = select(Invoice).where(Invoice.id == invoice_id)\n    result = await session.execute(\n        stmt,\n        execution_options={\"schema_translate_map\": {None: schema}},\n    )\n    invoice = result.scalar_one_or_none()\n    if invoice:\n        # ... dispatch email via SMTP or SES\n        pass\n\n\n@app.post(\"\u002Finvoices\u002F{invoice_id}\u002Fsend\")\nasync def trigger_invoice_email(\n    invoice_id: int,\n    background_tasks: BackgroundTasks,\n    schema: str = Depends(resolve_tenant_schema),\n    session: AsyncSession = Depends(get_db),\n):\n    background_tasks.add_task(\n        send_invoice_email_background, session, schema, invoice_id\n    )\n    return {\"queued\": True}\n",[18,1357,1358,1369,1379,1383,1387,1408,1413,1423,1433,1437,1451,1461,1465,1481,1485,1495,1502,1507,1512,1516,1520,1538,1549,1558,1563,1573,1581,1585,1590,1595,1599],{"__ignoreMap":46},[50,1359,1360,1362,1364,1366],{"class":52,"line":53},[50,1361,120],{"class":67},[50,1363,542],{"class":63},[50,1365,126],{"class":67},[50,1367,1368],{"class":63}," BackgroundTasks\n",[50,1370,1371,1373,1375,1377],{"class":52,"line":60},[50,1372,120],{"class":67},[50,1374,136],{"class":63},[50,1376,126],{"class":67},[50,1378,141],{"class":63},[50,1380,1381],{"class":52,"line":75},[50,1382,163],{"emptyLinePlaceholder":162},[50,1384,1385],{"class":52,"line":102},[50,1386,163],{"emptyLinePlaceholder":162},[50,1388,1389,1391,1393,1396,1399,1401,1404,1406],{"class":52,"line":159},[50,1390,169],{"class":67},[50,1392,172],{"class":67},[50,1394,1395],{"class":175}," send_invoice_email_background",[50,1397,1398],{"class":63},"(session: AsyncSession, schema: ",[50,1400,182],{"class":89},[50,1402,1403],{"class":63},", invoice_id: ",[50,1405,1112],{"class":89},[50,1407,894],{"class":63},[50,1409,1410],{"class":52,"line":166},[50,1411,1412],{"class":71},"    \"\"\"Background task: fetch invoice from tenant schema and send email.\"\"\"\n",[50,1414,1415,1417,1419,1421],{"class":52,"line":188},[50,1416,900],{"class":67},[50,1418,123],{"class":63},[50,1420,126],{"class":67},[50,1422,129],{"class":63},[50,1424,1425,1427,1429,1431],{"class":52,"line":208},[50,1426,900],{"class":67},[50,1428,148],{"class":63},[50,1430,126],{"class":67},[50,1432,907],{"class":63},[50,1434,1435],{"class":52,"line":222},[50,1436,163],{"emptyLinePlaceholder":162},[50,1438,1439,1441,1443,1446,1448],{"class":52,"line":228},[50,1440,191],{"class":63},[50,1442,68],{"class":67},[50,1444,1445],{"class":63}," select(Invoice).where(Invoice.id ",[50,1447,199],{"class":67},[50,1449,1450],{"class":63}," invoice_id)\n",[50,1452,1453,1455,1457,1459],{"class":52,"line":251},[50,1454,211],{"class":63},[50,1456,68],{"class":67},[50,1458,216],{"class":67},[50,1460,219],{"class":63},[50,1462,1463],{"class":52,"line":257},[50,1464,225],{"class":63},[50,1466,1467,1469,1471,1473,1475,1477,1479],{"class":52,"line":266},[50,1468,232],{"class":231},[50,1470,68],{"class":67},[50,1472,90],{"class":63},[50,1474,239],{"class":71},[50,1476,242],{"class":63},[50,1478,245],{"class":89},[50,1480,962],{"class":63},[50,1482,1483],{"class":52,"line":468},[50,1484,254],{"class":63},[50,1486,1487,1490,1492],{"class":52,"line":474},[50,1488,1489],{"class":63},"    invoice ",[50,1491,68],{"class":67},[50,1493,1494],{"class":63}," result.scalar_one_or_none()\n",[50,1496,1497,1499],{"class":52,"line":480},[50,1498,779],{"class":67},[50,1500,1501],{"class":63}," invoice:\n",[50,1503,1504],{"class":52,"line":491},[50,1505,1506],{"class":56},"        # ... dispatch email via SMTP or SES\n",[50,1508,1509],{"class":52,"line":497},[50,1510,1511],{"class":67},"        pass\n",[50,1513,1514],{"class":52,"line":508},[50,1515,163],{"emptyLinePlaceholder":162},[50,1517,1518],{"class":52,"line":521},[50,1519,163],{"emptyLinePlaceholder":162},[50,1521,1522,1525,1527,1530,1533,1536],{"class":52,"line":728},[50,1523,1524],{"class":175},"@app.post",[50,1526,846],{"class":63},[50,1528,1529],{"class":71},"\"\u002Finvoices\u002F",[50,1531,1532],{"class":89},"{invoice_id}",[50,1534,1535],{"class":71},"\u002Fsend\"",[50,1537,205],{"class":63},[50,1539,1540,1542,1544,1547],{"class":52,"line":740},[50,1541,169],{"class":67},[50,1543,172],{"class":67},[50,1545,1546],{"class":175}," trigger_invoice_email",[50,1548,737],{"class":63},[50,1550,1551,1554,1556],{"class":52,"line":751},[50,1552,1553],{"class":63},"    invoice_id: ",[50,1555,1112],{"class":89},[50,1557,421],{"class":63},[50,1559,1560],{"class":52,"line":762},[50,1561,1562],{"class":63},"    background_tasks: BackgroundTasks,\n",[50,1564,1565,1567,1569,1571],{"class":52,"line":776},[50,1566,869],{"class":63},[50,1568,182],{"class":89},[50,1570,874],{"class":67},[50,1572,877],{"class":63},[50,1574,1575,1577,1579],{"class":52,"line":793},[50,1576,883],{"class":63},[50,1578,68],{"class":67},[50,1580,888],{"class":63},[50,1582,1583],{"class":52,"line":822},[50,1584,894],{"class":63},[50,1586,1587],{"class":52,"line":830},[50,1588,1589],{"class":63},"    background_tasks.add_task(\n",[50,1591,1592],{"class":52,"line":835},[50,1593,1594],{"class":63},"        send_invoice_email_background, session, schema, invoice_id\n",[50,1596,1597],{"class":52,"line":840},[50,1598,254],{"class":63},[50,1600,1601,1603,1606,1609,1611,1613],{"class":52,"line":854},[50,1602,260],{"class":67},[50,1604,1605],{"class":63}," {",[50,1607,1608],{"class":71},"\"queued\"",[50,1610,633],{"class":63},[50,1612,455],{"class":89},[50,1614,667],{"class":63},[14,1616,1617,1618,1621,1622,1625],{},"Note that the session passed to a ",[18,1619,1620],{},"BackgroundTasks"," callback must remain open for the duration of the background task. Using ",[18,1623,1624],{},"async with AsyncSessionLocal() as session"," inside the background function (rather than injecting the request-scoped session) is safer for long-running tasks, because it prevents holding the session open past response delivery.",[33,1627,1629],{"id":1628},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1631,1632,1633,1649],"table",{},[1634,1635,1636],"thead",{},[1637,1638,1639,1643,1646],"tr",{},[1640,1641,1642],"th",{},"Error \u002F Warning",[1640,1644,1645],{},"Root Cause",[1640,1647,1648],{},"Production Fix",[1650,1651,1652,1676,1689,1726,1749,1769],"tbody",{},[1637,1653,1654,1660,1669],{},[1655,1656,1657],"td",{},[18,1658,1659],{},"sqlalchemy.exc.ProgrammingError: relation \"invoices\" does not exist",[1655,1661,1662,1664,1665,1668],{},[18,1663,320],{}," not applied to the statement, so SQL targets the default public schema which has no ",[18,1666,1667],{},"invoices"," table.",[1655,1670,1671,1672,1675],{},"Ensure every statement against schema'd models passes ",[18,1673,1674],{},"execution_options={\"schema_translate_map\": {...}}",". Consider a middleware layer that injects the map automatically.",[1637,1677,1678,1683,1686],{},[1655,1679,1680],{},[18,1681,1682],{},"sqlalchemy.exc.ProgrammingError: schema \"badschema\" does not exist",[1655,1684,1685],{},"An unvalidated tenant identifier was passed to the map.",[1655,1687,1688],{},"Validate tenant IDs against an allowlist before constructing the map. Never pass raw request headers directly.",[1637,1690,1691,1699,1714],{},[1655,1692,1693,1696,1697],{},[18,1694,1695],{},"KeyError: 'acme'"," on ",[18,1698,320],{},[1655,1700,1701,1702,1704,1705,1707,1708,1711,1712,293],{},"The map key does not match the ",[18,1703,93],{}," string in ",[18,1706,281],{},". E.g. model has ",[18,1709,1710],{},"schema=\"tenant\""," but map uses ",[18,1713,324],{},[1655,1715,1716,1717,1719,1720,1722,1723,293],{},"Match the map key to the exact string in ",[18,1718,281],{},". Use ",[18,1721,245],{}," only for models with no schema or ",[18,1724,1725],{},"schema=None",[1637,1727,1728,1733,1738],{},[1655,1729,1730],{},[18,1731,1732],{},"sqlalchemy.exc.InvalidRequestError: Session is already flushing",[1655,1734,1735,1737],{},[18,1736,320],{}," applied inside an autoflush cycle triggered by relationship traversal.",[1655,1739,1740,1741,1744,1745,1748],{},"Set ",[18,1742,1743],{},"autoflush=False"," on the session factory or wrap schema-routed operations in explicit ",[18,1746,1747],{},"async with session.begin():"," blocks.",[1637,1750,1751,1754,1759],{},[1655,1752,1753],{},"Stale cached SQL targeting the wrong schema",[1655,1755,1756,1758],{},[18,1757,320],{}," changed between calls but the same compiled cache entry was reused.",[1655,1760,1761,1762,1765,1766,1768],{},"This cannot happen — the map value is part of the cache key. If you observe this, check that ",[18,1763,1764],{},"execution_options"," is passed as a kwarg to ",[18,1767,24],{},", not set on the engine globally.",[1637,1770,1771,1777,1786],{},[1655,1772,1773,1776],{},[18,1774,1775],{},"asyncpg.exceptions.UndefinedTableError"," after PgBouncer migration",[1655,1778,1779,1780,1783,1784,293],{},"Previously using ",[18,1781,1782],{},"search_path"," which leaked across pooled connections; now connections arrive without the expected ",[18,1785,1782],{},[1655,1787,1788,1789,1791,1792,1795],{},"Migrate all schema routing to ",[18,1790,320],{},". Remove ",[18,1793,1794],{},"SET search_path"," from connection setup code.",[33,1797,1799],{"id":1798},"advanced-schema_translate_map-optimization","Advanced schema_translate_map Optimization",[299,1801,1803],{"id":1802},"middleware-level-schema-injection","Middleware-Level Schema Injection",[14,1805,1806,1807,1810],{},"Rather than threading ",[18,1808,1809],{},"tenant_schema"," through every function call, a Starlette middleware can attach the resolved schema to the request state and a session factory wrapper can read it automatically:",[41,1812,1814],{"className":43,"code":1813,"language":45,"meta":46,"style":46},"from starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\n\n\nclass TenantSchemaMiddleware(BaseHTTPMiddleware):\n    async def dispatch(self, request: Request, call_next):\n        tenant_id = request.headers.get(\"X-Tenant-ID\", \"\")\n        schema = VALID_SCHEMAS.get(tenant_id)\n        if schema is None and request.url.path.startswith(\"\u002Fapi\u002F\"):\n            from starlette.responses import JSONResponse\n            return JSONResponse({\"detail\": \"Unknown tenant\"}, status_code=403)\n        request.state.tenant_schema = schema\n        return await call_next(request)\n\n\nasync def get_schema(request: Request) -> str:\n    return request.state.tenant_schema\n\n\nasync def get_tenant_db(\n    session: AsyncSession = Depends(get_db),\n    schema: str = Depends(get_schema),\n):\n    \"\"\"Yields a session with tenant schema pre-bound in session.info.\"\"\"\n    session.info[\"schema\"] = schema\n    yield session\n",[18,1815,1816,1828,1840,1844,1848,1863,1875,1895,1907,1929,1942,1968,1977,1987,1991,1995,2011,2018,2022,2026,2037,2045,2056,2060,2065,2079],{"__ignoreMap":46},[50,1817,1818,1820,1823,1825],{"class":52,"line":53},[50,1819,120],{"class":67},[50,1821,1822],{"class":63}," starlette.middleware.base ",[50,1824,126],{"class":67},[50,1826,1827],{"class":63}," BaseHTTPMiddleware\n",[50,1829,1830,1832,1835,1837],{"class":52,"line":60},[50,1831,120],{"class":67},[50,1833,1834],{"class":63}," starlette.requests ",[50,1836,126],{"class":67},[50,1838,1839],{"class":63}," Request\n",[50,1841,1842],{"class":52,"line":75},[50,1843,163],{"emptyLinePlaceholder":162},[50,1845,1846],{"class":52,"line":102},[50,1847,163],{"emptyLinePlaceholder":162},[50,1849,1850,1853,1856,1858,1861],{"class":52,"line":159},[50,1851,1852],{"class":67},"class",[50,1854,1855],{"class":175}," TenantSchemaMiddleware",[50,1857,846],{"class":63},[50,1859,1860],{"class":175},"BaseHTTPMiddleware",[50,1862,894],{"class":63},[50,1864,1865,1867,1869,1872],{"class":52,"line":166},[50,1866,697],{"class":67},[50,1868,172],{"class":67},[50,1870,1871],{"class":175}," dispatch",[50,1873,1874],{"class":63},"(self, request: Request, call_next):\n",[50,1876,1877,1880,1882,1885,1888,1890,1893],{"class":52,"line":188},[50,1878,1879],{"class":63},"        tenant_id ",[50,1881,68],{"class":67},[50,1883,1884],{"class":63}," request.headers.get(",[50,1886,1887],{"class":71},"\"X-Tenant-ID\"",[50,1889,615],{"class":63},[50,1891,1892],{"class":71},"\"\"",[50,1894,205],{"class":63},[50,1896,1897,1900,1902,1904],{"class":52,"line":208},[50,1898,1899],{"class":63},"        schema ",[50,1901,68],{"class":67},[50,1903,770],{"class":89},[50,1905,1906],{"class":63},".get(tenant_id)\n",[50,1908,1909,1912,1914,1916,1918,1921,1924,1927],{"class":52,"line":222},[50,1910,1911],{"class":67},"        if",[50,1913,782],{"class":63},[50,1915,785],{"class":67},[50,1917,788],{"class":89},[50,1919,1920],{"class":67}," and",[50,1922,1923],{"class":63}," request.url.path.startswith(",[50,1925,1926],{"class":71},"\"\u002Fapi\u002F\"",[50,1928,894],{"class":63},[50,1930,1931,1934,1937,1939],{"class":52,"line":228},[50,1932,1933],{"class":67},"            from",[50,1935,1936],{"class":63}," starlette.responses ",[50,1938,126],{"class":67},[50,1940,1941],{"class":63}," JSONResponse\n",[50,1943,1944,1947,1950,1953,1955,1957,1960,1962,1964,1966],{"class":52,"line":251},[50,1945,1946],{"class":67},"            return",[50,1948,1949],{"class":63}," JSONResponse({",[50,1951,1952],{"class":71},"\"detail\"",[50,1954,633],{"class":63},[50,1956,817],{"class":71},[50,1958,1959],{"class":63},"}, ",[50,1961,802],{"class":231},[50,1963,68],{"class":67},[50,1965,807],{"class":89},[50,1967,205],{"class":63},[50,1969,1970,1973,1975],{"class":52,"line":257},[50,1971,1972],{"class":63},"        request.state.tenant_schema ",[50,1974,68],{"class":67},[50,1976,827],{"class":63},[50,1978,1979,1982,1984],{"class":52,"line":266},[50,1980,1981],{"class":67},"        return",[50,1983,216],{"class":67},[50,1985,1986],{"class":63}," call_next(request)\n",[50,1988,1989],{"class":52,"line":468},[50,1990,163],{"emptyLinePlaceholder":162},[50,1992,1993],{"class":52,"line":474},[50,1994,163],{"emptyLinePlaceholder":162},[50,1996,1997,1999,2001,2004,2007,2009],{"class":52,"line":480},[50,1998,169],{"class":67},[50,2000,172],{"class":67},[50,2002,2003],{"class":175}," get_schema",[50,2005,2006],{"class":63},"(request: Request) -> ",[50,2008,182],{"class":89},[50,2010,759],{"class":63},[50,2012,2013,2015],{"class":52,"line":491},[50,2014,260],{"class":67},[50,2016,2017],{"class":63}," request.state.tenant_schema\n",[50,2019,2020],{"class":52,"line":497},[50,2021,163],{"emptyLinePlaceholder":162},[50,2023,2024],{"class":52,"line":508},[50,2025,163],{"emptyLinePlaceholder":162},[50,2027,2028,2030,2032,2035],{"class":52,"line":521},[50,2029,169],{"class":67},[50,2031,172],{"class":67},[50,2033,2034],{"class":175}," get_tenant_db",[50,2036,737],{"class":63},[50,2038,2039,2041,2043],{"class":52,"line":728},[50,2040,883],{"class":63},[50,2042,68],{"class":67},[50,2044,888],{"class":63},[50,2046,2047,2049,2051,2053],{"class":52,"line":740},[50,2048,869],{"class":63},[50,2050,182],{"class":89},[50,2052,874],{"class":67},[50,2054,2055],{"class":63}," Depends(get_schema),\n",[50,2057,2058],{"class":52,"line":751},[50,2059,894],{"class":63},[50,2061,2062],{"class":52,"line":762},[50,2063,2064],{"class":71},"    \"\"\"Yields a session with tenant schema pre-bound in session.info.\"\"\"\n",[50,2066,2067,2070,2073,2075,2077],{"class":52,"line":776},[50,2068,2069],{"class":63},"    session.info[",[50,2071,2072],{"class":71},"\"schema\"",[50,2074,620],{"class":63},[50,2076,68],{"class":67},[50,2078,827],{"class":63},[50,2080,2081,2084],{"class":52,"line":793},[50,2082,2083],{"class":67},"    yield",[50,2085,717],{"class":63},[14,2087,2088,2089,2092,2093,2096],{},"All route handlers that depend on ",[18,2090,2091],{},"get_tenant_db"," can then call ",[18,2094,2095],{},"tenant_exec_opts(session)"," without caring where the schema name came from. This eliminates duplicated schema-resolution logic across dozens of endpoints.",[299,2098,2100],{"id":2099},"caching-the-resolved-schema-in-sessioninfo","Caching the Resolved Schema in session.info",[14,2102,2103,2104,2106],{},"For endpoints that call multiple ORM helpers, resolving and passing ",[18,2105,1809],{}," through every function signature is tedious. Attach it once per session and let helpers read it:",[41,2108,2110],{"className":43,"code":2109,"language":45,"meta":46,"style":46},"from sqlalchemy.ext.asyncio import AsyncSession\n\n\nasync def get_tenant_session(tenant_id: str) -> AsyncSession:\n    schema = VALID_SCHEMAS[tenant_id]\n    session = AsyncSessionLocal()\n    session.info[\"schema\"] = schema\n    return session\n\n\ndef tenant_exec_opts(session: AsyncSession) -> dict:\n    return {\"schema_translate_map\": {None: session.info[\"schema\"]}}\n\n\n# In any repository function:\nasync def count_invoices(session: AsyncSession) -> int:\n    from sqlalchemy import func, select\n    from your_models import Invoice\n\n    stmt = select(func.count()).select_from(Invoice)\n    result = await session.execute(stmt, execution_options=tenant_exec_opts(session))\n    return result.scalar_one()\n",[18,2111,2112,2122,2126,2130,2147,2158,2168,2180,2186,2190,2194,2209,2229,2233,2237,2242,2257,2268,2278,2282,2291,2309],{"__ignoreMap":46},[50,2113,2114,2116,2118,2120],{"class":52,"line":53},[50,2115,120],{"class":67},[50,2117,136],{"class":63},[50,2119,126],{"class":67},[50,2121,141],{"class":63},[50,2123,2124],{"class":52,"line":60},[50,2125,163],{"emptyLinePlaceholder":162},[50,2127,2128],{"class":52,"line":75},[50,2129,163],{"emptyLinePlaceholder":162},[50,2131,2132,2134,2136,2139,2142,2144],{"class":52,"line":102},[50,2133,169],{"class":67},[50,2135,172],{"class":67},[50,2137,2138],{"class":175}," get_tenant_session",[50,2140,2141],{"class":63},"(tenant_id: ",[50,2143,182],{"class":89},[50,2145,2146],{"class":63},") -> AsyncSession:\n",[50,2148,2149,2151,2153,2155],{"class":52,"line":159},[50,2150,765],{"class":63},[50,2152,68],{"class":67},[50,2154,770],{"class":89},[50,2156,2157],{"class":63},"[tenant_id]\n",[50,2159,2160,2163,2165],{"class":52,"line":166},[50,2161,2162],{"class":63},"    session ",[50,2164,68],{"class":67},[50,2166,2167],{"class":63}," AsyncSessionLocal()\n",[50,2169,2170,2172,2174,2176,2178],{"class":52,"line":188},[50,2171,2069],{"class":63},[50,2173,2072],{"class":71},[50,2175,620],{"class":63},[50,2177,68],{"class":67},[50,2179,827],{"class":63},[50,2181,2182,2184],{"class":52,"line":208},[50,2183,260],{"class":67},[50,2185,717],{"class":63},[50,2187,2188],{"class":52,"line":222},[50,2189,163],{"emptyLinePlaceholder":162},[50,2191,2192],{"class":52,"line":228},[50,2193,163],{"emptyLinePlaceholder":162},[50,2195,2196,2198,2201,2204,2207],{"class":52,"line":251},[50,2197,731],{"class":67},[50,2199,2200],{"class":175}," tenant_exec_opts",[50,2202,2203],{"class":63},"(session: AsyncSession) -> ",[50,2205,2206],{"class":89},"dict",[50,2208,759],{"class":63},[50,2210,2211,2213,2215,2217,2219,2221,2224,2226],{"class":52,"line":257},[50,2212,260],{"class":67},[50,2214,1605],{"class":63},[50,2216,239],{"class":71},[50,2218,242],{"class":63},[50,2220,245],{"class":89},[50,2222,2223],{"class":63},": session.info[",[50,2225,2072],{"class":71},[50,2227,2228],{"class":63},"]}}\n",[50,2230,2231],{"class":52,"line":266},[50,2232,163],{"emptyLinePlaceholder":162},[50,2234,2235],{"class":52,"line":468},[50,2236,163],{"emptyLinePlaceholder":162},[50,2238,2239],{"class":52,"line":474},[50,2240,2241],{"class":56},"# In any repository function:\n",[50,2243,2244,2246,2248,2251,2253,2255],{"class":52,"line":480},[50,2245,169],{"class":67},[50,2247,172],{"class":67},[50,2249,2250],{"class":175}," count_invoices",[50,2252,2203],{"class":63},[50,2254,1112],{"class":89},[50,2256,759],{"class":63},[50,2258,2259,2261,2263,2265],{"class":52,"line":491},[50,2260,900],{"class":67},[50,2262,123],{"class":63},[50,2264,126],{"class":67},[50,2266,2267],{"class":63}," func, select\n",[50,2269,2270,2272,2274,2276],{"class":52,"line":497},[50,2271,900],{"class":67},[50,2273,148],{"class":63},[50,2275,126],{"class":67},[50,2277,907],{"class":63},[50,2279,2280],{"class":52,"line":508},[50,2281,163],{"emptyLinePlaceholder":162},[50,2283,2284,2286,2288],{"class":52,"line":521},[50,2285,191],{"class":63},[50,2287,68],{"class":67},[50,2289,2290],{"class":63}," select(func.count()).select_from(Invoice)\n",[50,2292,2293,2295,2297,2299,2302,2304,2306],{"class":52,"line":728},[50,2294,211],{"class":63},[50,2296,68],{"class":67},[50,2298,216],{"class":67},[50,2300,2301],{"class":63}," session.execute(stmt, ",[50,2303,1764],{"class":231},[50,2305,68],{"class":67},[50,2307,2308],{"class":63},"tenant_exec_opts(session))\n",[50,2310,2311,2313],{"class":52,"line":740},[50,2312,260],{"class":67},[50,2314,1225],{"class":63},[14,2316,2317,2318,2321],{},"This pattern eliminates the risk of passing the wrong schema to an inner function and makes tenant context inspectable via ",[18,2319,2320],{},"session.info"," in logging middleware.",[299,2323,2325],{"id":2324},"combining-schema_translate_map-with-selectinload","Combining schema_translate_map with selectinload",[14,2327,2328,2329,2331,2332,2335,2336,2339],{},"Eager-loaded relationships also respect ",[18,2330,320],{}," when the relationship target model declares a matching schema. The map is applied to the secondary ",[18,2333,2334],{},"SELECT ... IN (...)"," query that ",[18,2337,2338],{},"selectinload"," emits:",[41,2341,2343],{"className":43,"code":2342,"language":45,"meta":46,"style":46},"from sqlalchemy.orm import selectinload\nfrom your_models import Tenant, Invoice\n\n\nasync def fetch_tenant_with_invoices(\n    session: AsyncSession, tenant_id: int, schema: str\n) -> Tenant:\n    stmt = (\n        select(Tenant)\n        .where(Tenant.id == tenant_id)\n        .options(selectinload(Tenant.invoices))\n    )\n    result = await session.execute(\n        stmt,\n        execution_options={\"schema_translate_map\": {None: schema}},\n    )\n    return result.scalar_one()\n    # Both SELECT tenants... and SELECT invoices WHERE tenant_id IN (...)\n    # are rewritten to the tenant schema.\n",[18,2344,2345,2357,2368,2372,2376,2387,2400,2405,2413,2418,2428,2433,2437,2447,2451,2467,2471,2477,2482],{"__ignoreMap":46},[50,2346,2347,2349,2352,2354],{"class":52,"line":53},[50,2348,120],{"class":67},[50,2350,2351],{"class":63}," sqlalchemy.orm ",[50,2353,126],{"class":67},[50,2355,2356],{"class":63}," selectinload\n",[50,2358,2359,2361,2363,2365],{"class":52,"line":60},[50,2360,120],{"class":67},[50,2362,148],{"class":63},[50,2364,126],{"class":67},[50,2366,2367],{"class":63}," Tenant, Invoice\n",[50,2369,2370],{"class":52,"line":75},[50,2371,163],{"emptyLinePlaceholder":162},[50,2373,2374],{"class":52,"line":102},[50,2375,163],{"emptyLinePlaceholder":162},[50,2377,2378,2380,2382,2385],{"class":52,"line":159},[50,2379,169],{"class":67},[50,2381,172],{"class":67},[50,2383,2384],{"class":175}," fetch_tenant_with_invoices",[50,2386,737],{"class":63},[50,2388,2389,2392,2394,2397],{"class":52,"line":166},[50,2390,2391],{"class":63},"    session: AsyncSession, tenant_id: ",[50,2393,1112],{"class":89},[50,2395,2396],{"class":63},", schema: ",[50,2398,2399],{"class":89},"str\n",[50,2401,2402],{"class":52,"line":188},[50,2403,2404],{"class":63},") -> Tenant:\n",[50,2406,2407,2409,2411],{"class":52,"line":208},[50,2408,191],{"class":63},[50,2410,68],{"class":67},[50,2412,380],{"class":63},[50,2414,2415],{"class":52,"line":222},[50,2416,2417],{"class":63},"        select(Tenant)\n",[50,2419,2420,2423,2425],{"class":52,"line":228},[50,2421,2422],{"class":63},"        .where(Tenant.id ",[50,2424,199],{"class":67},[50,2426,2427],{"class":63}," tenant_id)\n",[50,2429,2430],{"class":52,"line":251},[50,2431,2432],{"class":63},"        .options(selectinload(Tenant.invoices))\n",[50,2434,2435],{"class":52,"line":257},[50,2436,254],{"class":63},[50,2438,2439,2441,2443,2445],{"class":52,"line":266},[50,2440,211],{"class":63},[50,2442,68],{"class":67},[50,2444,216],{"class":67},[50,2446,219],{"class":63},[50,2448,2449],{"class":52,"line":468},[50,2450,225],{"class":63},[50,2452,2453,2455,2457,2459,2461,2463,2465],{"class":52,"line":474},[50,2454,232],{"class":231},[50,2456,68],{"class":67},[50,2458,90],{"class":63},[50,2460,239],{"class":71},[50,2462,242],{"class":63},[50,2464,245],{"class":89},[50,2466,962],{"class":63},[50,2468,2469],{"class":52,"line":480},[50,2470,254],{"class":63},[50,2472,2473,2475],{"class":52,"line":491},[50,2474,260],{"class":67},[50,2476,1225],{"class":63},[50,2478,2479],{"class":52,"line":497},[50,2480,2481],{"class":56},"    # Both SELECT tenants... and SELECT invoices WHERE tenant_id IN (...)\n",[50,2483,2484],{"class":52,"line":508},[50,2485,2486],{"class":56},"    # are rewritten to the tenant schema.\n",[299,2488,2490],{"id":2489},"validating-schema-names-before-use","Validating Schema Names Before Use",[14,2492,2493,2494,2497],{},"The single most important security control in schema-per-tenant routing is schema name validation. A schema name in PostgreSQL is an identifier, not a parameter — it cannot be safely passed through a ",[18,2495,2496],{},"%s"," placeholder. Always validate against one of these strategies:",[331,2499,2500,2511,2525],{},[334,2501,2502,2506,2507,2510],{},[2503,2504,2505],"strong",{},"Database allowlist",": Query ",[18,2508,2509],{},"SELECT schema_name FROM information_schema.schemata WHERE schema_name = $1"," before using the name. This is authoritative but adds a round-trip.",[334,2512,2513,2516,2517,2520,2521,2524],{},[2503,2514,2515],{},"In-memory allowlist",": Build a ",[18,2518,2519],{},"set[str]"," of valid schema names at application startup from the ",[18,2522,2523],{},"tenants"," table and refresh it periodically (every 60 seconds) via a background task.",[334,2526,2527,633,2530,2533],{},[2503,2528,2529],{},"Regex whitelist",[18,2531,2532],{},"re.fullmatch(r\"[a-z][a-z0-9_]{0,62}\", schema_name)"," rejects all SQL injection payloads while allowing valid PostgreSQL identifiers. Use this as a secondary guard, not a substitute for the database allowlist.",[41,2535,2537],{"className":43,"code":2536,"language":45,"meta":46,"style":46},"import re\nfrom functools import lru_cache\n\n\n_VALID_SCHEMA_PATTERN = re.compile(r\"^[a-z][a-z0-9_]{0,62}$\")\n_schema_allowlist: set[str] = set()\n\n\ndef validate_schema(schema: str) -> str:\n    if not _VALID_SCHEMA_PATTERN.match(schema):\n        raise ValueError(f\"Invalid schema name format: {schema!r}\")\n    if schema not in _schema_allowlist:\n        raise LookupError(f\"Unknown tenant schema: {schema!r}\")\n    return schema\n",[18,2538,2539,2546,2558,2562,2566,2595,2612,2616,2620,2638,2651,2679,2694,2720],{"__ignoreMap":46},[50,2540,2541,2543],{"class":52,"line":53},[50,2542,126],{"class":67},[50,2544,2545],{"class":63}," re\n",[50,2547,2548,2550,2553,2555],{"class":52,"line":60},[50,2549,120],{"class":67},[50,2551,2552],{"class":63}," functools ",[50,2554,126],{"class":67},[50,2556,2557],{"class":63}," lru_cache\n",[50,2559,2560],{"class":52,"line":75},[50,2561,163],{"emptyLinePlaceholder":162},[50,2563,2564],{"class":52,"line":102},[50,2565,163],{"emptyLinePlaceholder":162},[50,2567,2568,2571,2573,2576,2579,2582,2585,2588,2591,2593],{"class":52,"line":159},[50,2569,2570],{"class":89},"_VALID_SCHEMA_PATTERN",[50,2572,874],{"class":67},[50,2574,2575],{"class":63}," re.compile(",[50,2577,2578],{"class":67},"r",[50,2580,2581],{"class":71},"\"",[50,2583,2584],{"class":89},"^[a-z][a-z0-9_]",[50,2586,2587],{"class":67},"{0,62}",[50,2589,2590],{"class":89},"$",[50,2592,2581],{"class":71},[50,2594,205],{"class":63},[50,2596,2597,2600,2602,2604,2606,2609],{"class":52,"line":166},[50,2598,2599],{"class":63},"_schema_allowlist: set[",[50,2601,182],{"class":89},[50,2603,620],{"class":63},[50,2605,68],{"class":67},[50,2607,2608],{"class":89}," set",[50,2610,2611],{"class":63},"()\n",[50,2613,2614],{"class":52,"line":188},[50,2615,163],{"emptyLinePlaceholder":162},[50,2617,2618],{"class":52,"line":208},[50,2619,163],{"emptyLinePlaceholder":162},[50,2621,2622,2624,2627,2630,2632,2634,2636],{"class":52,"line":222},[50,2623,731],{"class":67},[50,2625,2626],{"class":175}," validate_schema",[50,2628,2629],{"class":63},"(schema: ",[50,2631,182],{"class":89},[50,2633,754],{"class":63},[50,2635,182],{"class":89},[50,2637,759],{"class":63},[50,2639,2640,2642,2645,2648],{"class":52,"line":228},[50,2641,779],{"class":67},[50,2643,2644],{"class":67}," not",[50,2646,2647],{"class":89}," _VALID_SCHEMA_PATTERN",[50,2649,2650],{"class":63},".match(schema):\n",[50,2652,2653,2655,2658,2660,2663,2666,2668,2670,2673,2675,2677],{"class":52,"line":251},[50,2654,796],{"class":67},[50,2656,2657],{"class":89}," ValueError",[50,2659,846],{"class":63},[50,2661,2662],{"class":67},"f",[50,2664,2665],{"class":71},"\"Invalid schema name format: ",[50,2667,90],{"class":89},[50,2669,93],{"class":63},[50,2671,2672],{"class":67},"!r",[50,2674,96],{"class":89},[50,2676,2581],{"class":71},[50,2678,205],{"class":63},[50,2680,2681,2683,2685,2688,2691],{"class":52,"line":257},[50,2682,779],{"class":67},[50,2684,782],{"class":63},[50,2686,2687],{"class":67},"not",[50,2689,2690],{"class":67}," in",[50,2692,2693],{"class":63}," _schema_allowlist:\n",[50,2695,2696,2698,2701,2703,2705,2708,2710,2712,2714,2716,2718],{"class":52,"line":266},[50,2697,796],{"class":67},[50,2699,2700],{"class":89}," LookupError",[50,2702,846],{"class":63},[50,2704,2662],{"class":67},[50,2706,2707],{"class":71},"\"Unknown tenant schema: ",[50,2709,90],{"class":89},[50,2711,93],{"class":63},[50,2713,2672],{"class":67},[50,2715,96],{"class":89},[50,2717,2581],{"class":71},[50,2719,205],{"class":63},[50,2721,2722,2724],{"class":52,"line":468},[50,2723,260],{"class":67},[50,2725,827],{"class":63},[14,2727,2728,2729,2732],{},"Refresh ",[18,2730,2731],{},"_schema_allowlist"," from a background asyncio task that runs every 60 seconds:",[41,2734,2736],{"className":43,"code":2735,"language":45,"meta":46,"style":46},"import asyncio\nfrom sqlalchemy import text\n\n\nasync def refresh_schema_allowlist(engine) -> None:\n    while True:\n        async with engine.connect() as conn:\n            result = await conn.execute(text(\"SELECT schema_name FROM tenants\"))\n            _schema_allowlist.clear()\n            _schema_allowlist.update(row[0] for row in result)\n        await asyncio.sleep(60)\n",[18,2737,2738,2745,2756,2760,2764,2780,2790,2805,2823,2828,2848],{"__ignoreMap":46},[50,2739,2740,2742],{"class":52,"line":53},[50,2741,126],{"class":67},[50,2743,2744],{"class":63}," asyncio\n",[50,2746,2747,2749,2751,2753],{"class":52,"line":60},[50,2748,120],{"class":67},[50,2750,123],{"class":63},[50,2752,126],{"class":67},[50,2754,2755],{"class":63}," text\n",[50,2757,2758],{"class":52,"line":75},[50,2759,163],{"emptyLinePlaceholder":162},[50,2761,2762],{"class":52,"line":102},[50,2763,163],{"emptyLinePlaceholder":162},[50,2765,2766,2768,2770,2773,2776,2778],{"class":52,"line":159},[50,2767,169],{"class":67},[50,2769,172],{"class":67},[50,2771,2772],{"class":175}," refresh_schema_allowlist",[50,2774,2775],{"class":63},"(engine) -> ",[50,2777,245],{"class":89},[50,2779,759],{"class":63},[50,2781,2782,2785,2788],{"class":52,"line":166},[50,2783,2784],{"class":67},"    while",[50,2786,2787],{"class":89}," True",[50,2789,759],{"class":63},[50,2791,2792,2795,2797,2800,2802],{"class":52,"line":188},[50,2793,2794],{"class":67},"        async",[50,2796,700],{"class":67},[50,2798,2799],{"class":63}," engine.connect() ",[50,2801,706],{"class":67},[50,2803,2804],{"class":63}," conn:\n",[50,2806,2807,2810,2812,2814,2817,2820],{"class":52,"line":208},[50,2808,2809],{"class":63},"            result ",[50,2811,68],{"class":67},[50,2813,216],{"class":67},[50,2815,2816],{"class":63}," conn.execute(text(",[50,2818,2819],{"class":71},"\"SELECT schema_name FROM tenants\"",[50,2821,2822],{"class":63},"))\n",[50,2824,2825],{"class":52,"line":222},[50,2826,2827],{"class":63},"            _schema_allowlist.clear()\n",[50,2829,2830,2833,2836,2838,2840,2843,2845],{"class":52,"line":228},[50,2831,2832],{"class":63},"            _schema_allowlist.update(row[",[50,2834,2835],{"class":89},"0",[50,2837,620],{"class":63},[50,2839,1000],{"class":67},[50,2841,2842],{"class":63}," row ",[50,2844,1006],{"class":67},[50,2846,2847],{"class":63}," result)\n",[50,2849,2850,2853,2856,2859],{"class":52,"line":251},[50,2851,2852],{"class":67},"        await",[50,2854,2855],{"class":63}," asyncio.sleep(",[50,2857,2858],{"class":89},"60",[50,2860,205],{"class":63},[33,2862,2864],{"id":2863},"frequently-asked-questions","Frequently Asked Questions",[14,2866,2867,2880,325,2882,2884,2885,2887,2888,2891,2892,2895,2896,293],{},[2503,2868,2869,2870,2872,2873,325,2876,2879],{},"Does ",[18,2871,320],{}," work with ",[18,2874,2875],{},"session.get()",[18,2877,2878],{},"session.merge()","?",[18,2881,2875],{},[18,2883,2878],{}," do not accept ",[18,2886,1764],{}," directly. The map must be pre-configured at the connection level using ",[18,2889,2890],{},"conn.execution_options(schema_translate_map=...)"," before passing the connection to the session, or you must use ",[18,2893,2894],{},"session.execute(select(Model).where(...))"," instead of ",[18,2897,2875],{},[14,2899,2900,2906,2907,2910],{},[2503,2901,2902,2903,2905],{},"Can I apply ",[18,2904,320],{}," to multiple schema keys at once?","\nYes. The map is a plain Python dict. ",[18,2908,2909],{},"{None: \"acme\", \"analytics\": \"acme_analytics\", \"audit\": \"acme_audit\"}"," redirects three different model schemas simultaneously in a single statement.",[14,2912,2913,2916,2917,2920,2921,2924,2925,2928],{},[2503,2914,2915],{},"How do I verify which SQL is actually emitted?","\nEnable engine echo: ",[18,2918,2919],{},"create_async_engine(..., echo=True)",". In production, attach an event listener to ",[18,2922,2923],{},"engine.sync_engine"," using ",[18,2926,2927],{},"event.listen(engine.sync_engine, \"before_cursor_execute\", log_fn)"," to capture the compiled SQL string including schema-rewritten table names.",[14,2930,2931,2943,2944,2946,2947,2950],{},[2503,2932,2869,2933,2935,2936,309,2939,2942],{},[18,2934,320],{}," affect the ",[18,2937,2938],{},"information_schema",[18,2940,2941],{},"pg_catalog"," queries SQLAlchemy runs internally?","\nNo. SQLAlchemy's internal reflection and catalog queries use hardcoded schema names that are not subject to ",[18,2945,320],{}," rewriting. The map only applies to user-defined ",[18,2948,2949],{},"Table"," objects.",[33,2952,2954],{"id":2953},"related","Related",[2956,2957,2958,2963,2970,2977],"ul",{},[334,2959,2960,2962],{},[27,2961,30],{"href":29}," — Parent guide covering schema isolation models, engine-per-tenant patterns, and vertical partitioning.",[334,2964,2965,2969],{},[27,2966,2968],{"href":2967},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Frouting-reads-to-replicas-with-async-engines\u002F","Routing Reads to Replicas with Async Engines"," — Combine replica routing with schema routing for full production multi-tenant architecture.",[334,2971,2972,2976],{},[27,2973,2975],{"href":2974},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","Async Engines, Dialects and Connection Pooling"," — Connection pool configuration that supports concurrent per-tenant requests safely.",[334,2978,2979,2983],{},[27,2980,2982],{"href":2981},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002F","Integrating SQLAlchemy Async with FastAPI and Starlette"," — Dependency injection patterns for session lifecycle management in FastAPI.",[2985,2986,2987],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":46,"searchDepth":60,"depth":60,"links":2989},[2990,2991,2999,3000,3006,3007],{"id":35,"depth":60,"text":36},{"id":296,"depth":60,"text":297,"children":2992},[2993,2994,2995,2996,2997,2998],{"id":301,"depth":75,"text":302},{"id":356,"depth":75,"text":357},{"id":526,"depth":75,"text":527},{"id":1012,"depth":75,"text":1013},{"id":1238,"depth":75,"text":1239},{"id":1348,"depth":75,"text":1349},{"id":1628,"depth":60,"text":1629},{"id":1798,"depth":60,"text":1799,"children":3001},[3002,3003,3004,3005],{"id":1802,"depth":75,"text":1803},{"id":2099,"depth":75,"text":2100},{"id":2324,"depth":75,"text":2325},{"id":2489,"depth":75,"text":2490},{"id":2863,"depth":60,"text":2864},{"id":2953,"depth":60,"text":2954},"Pass execution_options={\"schema_translate_map\": {None: tenant_schema}} to session.execute() on every statement to rewrite all unqualified table references to the target PostgreSQL schema at SQLAlchemy compile time — no model changes required. This is the recommended pattern in the Dynamic Schema and Multi-Tenant Routing architecture for SaaS applications that isolate tenants into separate schemas within a shared database.","md",{"date":3011},"2026-06-18","\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Fswitching-schemas-per-request-with-schema-translate-map",{"title":5,"description":3008},"advanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Fswitching-schemas-per-request-with-schema-translate-map\u002Findex","RensA3WxE7MV9Puh0WirJHqSoVmcJVw8N9Ma7J-su-M",1781810028979]