[{"data":1,"prerenderedAt":3297},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002F":3},{"id":4,"title":5,"body":6,"description":3289,"extension":3290,"meta":3291,"navigation":113,"path":3293,"seo":3294,"stem":3295,"__hash__":3296},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Findex.md","Dialect-Specific Gotchas and Driver Quirks in Async SQLAlchemy",{"type":7,"value":8,"toc":3264},"minimark",[9,13,23,35,40,58,73,262,272,276,281,292,299,307,310,394,409,424,428,438,444,447,517,532,536,540,550,745,752,832,840,844,855,866,954,1035,1038,1129,1133,1137,1144,1174,1344,1350,1472,1492,1572,1592,1731,1735,1746,1893,1900,1904,1917,1961,2069,2080,2150,2154,2172,2285,2295,2347,2351,2355,2358,2511,2525,2529,2535,2583,2705,2712,2716,2858,2862,3115,3119,3131,3144,3164,3190,3205,3209,3260],[10,11,5],"h1",{"id":12},"dialect-specific-gotchas-and-driver-quirks-in-async-sqlalchemy",[14,15,16,17,22],"p",{},"Async SQLAlchemy is not one thing — it is a thin async shim over four distinct database drivers, each with its own connection model, type encoding pipeline, and server negotiation protocol, all covered in the ",[18,19,21],"a",{"href":20},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","async engines, dialects, and connection pooling guide",".",[14,24,25,26,30,31,34],{},"When a ",[27,28,29],"code",{},"TimeoutError"," fires in production after two hours of stability, or Decimal values silently become floats, or every mutation raises ",[27,32,33],{},"OperationalError: cannot start a transaction within a transaction",", the root cause almost always lives in one of these driver-specific layers rather than in SQLAlchemy's ORM. This guide catalogues the most expensive lessons learned — covering asyncpg, psycopg3, aiosqlite, and aiomysql — so you can fix them before they reach production.",[36,37,39],"h2",{"id":38},"concept-execution-model","Concept & Execution Model",[14,41,42,43,46,47,46,50,53,54,57],{},"SQLAlchemy 2.0 abstracts over its async drivers through the ",[27,44,45],{},"AsyncEngine"," \u002F ",[27,48,49],{},"AsyncConnection",[27,51,52],{},"AsyncSession"," stack. Each call to ",[27,55,56],{},"async with AsyncSession(engine) as session"," ultimately calls into a driver-specific connection pool implementation. SQLAlchemy issues SQL as text or compiled constructs; the driver serialises that text and any bound parameters to the wire protocol.",[14,59,60,61,65,66,69,70,22],{},"The critical insight is that SQLAlchemy does ",[62,63,64],"strong",{},"not"," own the TCP connection lifecycle. The driver does. This means anything the driver does to a connection — caching a prepared statement, applying a server-side type codec, pinning a transaction mode — is invisible to SQLAlchemy's abstraction layer unless you explicitly thread it through ",[27,67,68],{},"connect_args"," or ",[27,71,72],{},"execution_options",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"language-python shiki shiki-themes github-light github-dark","# Async engine creation with driver-specific connect_args\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    connect_args={\n        # Driver-level knobs — not SQLAlchemy pool settings\n        \"statement_cache_size\": 0,\n        \"server_settings\": {\n            \"application_name\": \"my_service\",\n            \"jit\": \"off\",\n        },\n    },\n    pool_size=10,\n    max_overflow=20,\n    pool_pre_ping=True,\n)\n","python","",[27,81,82,91,108,115,127,137,149,155,170,179,192,205,211,217,230,243,256],{"__ignoreMap":79},[83,84,87],"span",{"class":85,"line":86},"line",1,[83,88,90],{"class":89},"sJ8bj","# Async engine creation with driver-specific connect_args\n",[83,92,94,98,102,105],{"class":85,"line":93},2,[83,95,97],{"class":96},"szBVR","from",[83,99,101],{"class":100},"sVt8B"," sqlalchemy.ext.asyncio ",[83,103,104],{"class":96},"import",[83,106,107],{"class":100}," create_async_engine\n",[83,109,111],{"class":85,"line":110},3,[83,112,114],{"emptyLinePlaceholder":113},true,"\n",[83,116,118,121,124],{"class":85,"line":117},4,[83,119,120],{"class":100},"engine ",[83,122,123],{"class":96},"=",[83,125,126],{"class":100}," create_async_engine(\n",[83,128,130,134],{"class":85,"line":129},5,[83,131,133],{"class":132},"sZZnC","    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[83,135,136],{"class":100},",\n",[83,138,140,144,146],{"class":85,"line":139},6,[83,141,143],{"class":142},"s4XuR","    connect_args",[83,145,123],{"class":96},[83,147,148],{"class":100},"{\n",[83,150,152],{"class":85,"line":151},7,[83,153,154],{"class":89},"        # Driver-level knobs — not SQLAlchemy pool settings\n",[83,156,158,161,164,168],{"class":85,"line":157},8,[83,159,160],{"class":132},"        \"statement_cache_size\"",[83,162,163],{"class":100},": ",[83,165,167],{"class":166},"sj4cs","0",[83,169,136],{"class":100},[83,171,173,176],{"class":85,"line":172},9,[83,174,175],{"class":132},"        \"server_settings\"",[83,177,178],{"class":100},": {\n",[83,180,182,185,187,190],{"class":85,"line":181},10,[83,183,184],{"class":132},"            \"application_name\"",[83,186,163],{"class":100},[83,188,189],{"class":132},"\"my_service\"",[83,191,136],{"class":100},[83,193,195,198,200,203],{"class":85,"line":194},11,[83,196,197],{"class":132},"            \"jit\"",[83,199,163],{"class":100},[83,201,202],{"class":132},"\"off\"",[83,204,136],{"class":100},[83,206,208],{"class":85,"line":207},12,[83,209,210],{"class":100},"        },\n",[83,212,214],{"class":85,"line":213},13,[83,215,216],{"class":100},"    },\n",[83,218,220,223,225,228],{"class":85,"line":219},14,[83,221,222],{"class":142},"    pool_size",[83,224,123],{"class":96},[83,226,227],{"class":166},"10",[83,229,136],{"class":100},[83,231,233,236,238,241],{"class":85,"line":232},15,[83,234,235],{"class":142},"    max_overflow",[83,237,123],{"class":96},[83,239,240],{"class":166},"20",[83,242,136],{"class":100},[83,244,246,249,251,254],{"class":85,"line":245},16,[83,247,248],{"class":142},"    pool_pre_ping",[83,250,123],{"class":96},[83,252,253],{"class":166},"True",[83,255,136],{"class":100},[83,257,259],{"class":85,"line":258},17,[83,260,261],{"class":100},")\n",[14,263,264,265,267,268,271],{},"The ",[27,266,68],{}," dict is passed verbatim to the underlying driver's ",[27,269,270],{},"connect()"," call. Every driver interprets this dict differently, which is the core source of confusion.",[36,273,275],{"id":274},"query-construction-async-execution-patterns","Query Construction & Async Execution Patterns",[277,278,280],"h3",{"id":279},"asyncpg-prepared-statement-caching-and-pgbouncer","asyncpg: Prepared Statement Caching and PgBouncer",[14,282,283,284,287,288,291],{},"asyncpg's defining feature is aggressive prepared-statement caching. On first execution of any query string, asyncpg sends a ",[27,285,286],{},"Parse"," message to PostgreSQL, which returns a named server-side prepared statement handle (e.g. ",[27,289,290],{},"__asyncpg_stmt_0__","). Subsequent executions reuse that handle, skipping the parse\u002Fplan phase.",[14,293,294,295,298],{},"This is enormously fast for a dedicated TCP connection. It is catastrophically wrong behind ",[62,296,297],{},"PgBouncer in transaction pooling mode",", where each transaction may land on a different backend server that has never seen your prepared statement. The error you see is:",[74,300,305],{"className":301,"code":303,"language":304},[302],"language-text","asyncpg.exceptions.InvalidSQLStatementNameError:\nprepared statement \"__asyncpg_stmt_0__\" does not exist\n","text",[27,306,303],{"__ignoreMap":79},[14,308,309],{},"The fix is to disable prepared statements at the driver level:",[74,311,313],{"className":76,"code":312,"language":78,"meta":79,"style":79},"# Async — asyncpg behind PgBouncer transaction pooling\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\",\n    connect_args={\n        \"statement_cache_size\": 0,           # disable client-side cache\n        \"prepared_statement_cache_size\": 0,  # asyncpg ≥ 0.28 explicit knob\n    },\n)\n",[27,314,315,320,330,334,342,349,357,371,386,390],{"__ignoreMap":79},[83,316,317],{"class":85,"line":86},[83,318,319],{"class":89},"# Async — asyncpg behind PgBouncer transaction pooling\n",[83,321,322,324,326,328],{"class":85,"line":93},[83,323,97],{"class":96},[83,325,101],{"class":100},[83,327,104],{"class":96},[83,329,107],{"class":100},[83,331,332],{"class":85,"line":110},[83,333,114],{"emptyLinePlaceholder":113},[83,335,336,338,340],{"class":85,"line":117},[83,337,120],{"class":100},[83,339,123],{"class":96},[83,341,126],{"class":100},[83,343,344,347],{"class":85,"line":129},[83,345,346],{"class":132},"    \"postgresql+asyncpg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\"",[83,348,136],{"class":100},[83,350,351,353,355],{"class":85,"line":139},[83,352,143],{"class":142},[83,354,123],{"class":96},[83,356,148],{"class":100},[83,358,359,361,363,365,368],{"class":85,"line":151},[83,360,160],{"class":132},[83,362,163],{"class":100},[83,364,167],{"class":166},[83,366,367],{"class":100},",           ",[83,369,370],{"class":89},"# disable client-side cache\n",[83,372,373,376,378,380,383],{"class":85,"line":157},[83,374,375],{"class":132},"        \"prepared_statement_cache_size\"",[83,377,163],{"class":100},[83,379,167],{"class":166},[83,381,382],{"class":100},",  ",[83,384,385],{"class":89},"# asyncpg ≥ 0.28 explicit knob\n",[83,387,388],{"class":85,"line":172},[83,389,216],{"class":100},[83,391,392],{"class":85,"line":181},[83,393,261],{"class":100},[14,395,396,397,400,401,404,405,408],{},"Both keys are required: ",[27,398,399],{},"statement_cache_size"," controls the LRU on the asyncpg ",[27,402,403],{},"Connection"," object, while ",[27,406,407],{},"prepared_statement_cache_size"," controls the newer per-connection cache introduced in asyncpg 0.28. Setting only one leaves the other active on newer driver versions.",[14,410,411,412,415,416,419,420,22],{},"For a full walkthrough of diagnosing and fixing prepared-statement errors — including ",[27,413,414],{},"DuplicatePreparedStatementError"," and the ",[27,417,418],{},"cannot insert multiple commands into a prepared statement"," variant — see the ",[18,421,423],{"href":422},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer\u002F","guide to handling asyncpg prepared statement errors with PgBouncer",[277,425,427],{"id":426},"psycopg3-binary-protocol-and-row-factory","psycopg3: Binary Protocol and Row Factory",[14,429,430,431,69,434,437],{},"psycopg3 (",[27,432,433],{},"psycopg[c]",[27,435,436],{},"psycopg[binary]",") defaults to the binary protocol for many types, which is faster but requires the server to support it. When connecting to PgBouncer (which does not speak binary protocol), or an older Postgres version, you may see:",[74,439,442],{"className":440,"code":441,"language":304},[302],"psycopg.errors.ProtocolViolation: expected data block, got message type 'E'\n",[27,443,441],{"__ignoreMap":79},[14,445,446],{},"Force text protocol via:",[74,448,450],{"className":76,"code":449,"language":78,"meta":79,"style":79},"# Async — psycopg3 with forced text protocol\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+psycopg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\",\n    connect_args={\n        \"binary\": False,  # disable binary protocol globally\n    },\n)\n",[27,451,452,457,467,471,479,486,494,509,513],{"__ignoreMap":79},[83,453,454],{"class":85,"line":86},[83,455,456],{"class":89},"# Async — psycopg3 with forced text protocol\n",[83,458,459,461,463,465],{"class":85,"line":93},[83,460,97],{"class":96},[83,462,101],{"class":100},[83,464,104],{"class":96},[83,466,107],{"class":100},[83,468,469],{"class":85,"line":110},[83,470,114],{"emptyLinePlaceholder":113},[83,472,473,475,477],{"class":85,"line":117},[83,474,120],{"class":100},[83,476,123],{"class":96},[83,478,126],{"class":100},[83,480,481,484],{"class":85,"line":129},[83,482,483],{"class":132},"    \"postgresql+psycopg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\"",[83,485,136],{"class":100},[83,487,488,490,492],{"class":85,"line":139},[83,489,143],{"class":142},[83,491,123],{"class":96},[83,493,148],{"class":100},[83,495,496,499,501,504,506],{"class":85,"line":151},[83,497,498],{"class":132},"        \"binary\"",[83,500,163],{"class":100},[83,502,503],{"class":166},"False",[83,505,382],{"class":100},[83,507,508],{"class":89},"# disable binary protocol globally\n",[83,510,511],{"class":85,"line":157},[83,512,216],{"class":100},[83,514,515],{"class":85,"line":172},[83,516,261],{"class":100},[14,518,519,520,523,524,527,528,531],{},"psycopg3 also ships with an async-native connection pool (",[27,521,522],{},"psycopg_pool.AsyncConnectionPool",") that SQLAlchemy can integrate with via ",[27,525,526],{},"create_async_engine(..., async_creator=...)",", bypassing SQLAlchemy's own pool entirely. This is useful when you need psycopg3-specific features like ",[27,529,530],{},"LISTEN\u002FNOTIFY"," support.",[36,533,535],{"id":534},"state-management-session-boundaries","State Management & Session Boundaries",[277,537,539],{"id":538},"aiosqlite-single-writer-locking","aiosqlite: Single-Writer Locking",[14,541,542,543,545,546,549],{},"SQLite enforces database-wide write locking. In async code, this creates a subtle trap: if two coroutines concurrently attempt writes inside separate ",[27,544,52],{}," contexts, one will block (or raise ",[27,547,548],{},"OperationalError: database is locked",") because aiosqlite serialises all operations through a background thread.",[74,551,553],{"className":76,"code":552,"language":78,"meta":79,"style":79},"# Wrong — concurrent writes will deadlock or raise\nimport asyncio\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_async_engine(\"sqlite+aiosqlite:\u002F\u002F\u002Forders.db\")\nAsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)\n\nasync def write_order(order_id: int) -> None:\n    async with AsyncSessionLocal() as session:\n        async with session.begin():\n            session.add(Order(id=order_id, status=\"pending\"))\n\n# These will contend on the single write lock\nawait asyncio.gather(write_order(1), write_order(2))\n",[27,554,555,560,567,578,590,594,608,635,639,666,683,693,717,721,726],{"__ignoreMap":79},[83,556,557],{"class":85,"line":86},[83,558,559],{"class":89},"# Wrong — concurrent writes will deadlock or raise\n",[83,561,562,564],{"class":85,"line":93},[83,563,104],{"class":96},[83,565,566],{"class":100}," asyncio\n",[83,568,569,571,573,575],{"class":85,"line":110},[83,570,97],{"class":96},[83,572,101],{"class":100},[83,574,104],{"class":96},[83,576,577],{"class":100}," AsyncSession, create_async_engine\n",[83,579,580,582,585,587],{"class":85,"line":117},[83,581,97],{"class":96},[83,583,584],{"class":100}," sqlalchemy.orm ",[83,586,104],{"class":96},[83,588,589],{"class":100}," sessionmaker\n",[83,591,592],{"class":85,"line":129},[83,593,114],{"emptyLinePlaceholder":113},[83,595,596,598,600,603,606],{"class":85,"line":139},[83,597,120],{"class":100},[83,599,123],{"class":96},[83,601,602],{"class":100}," create_async_engine(",[83,604,605],{"class":132},"\"sqlite+aiosqlite:\u002F\u002F\u002Forders.db\"",[83,607,261],{"class":100},[83,609,610,613,615,618,621,623,626,629,631,633],{"class":85,"line":151},[83,611,612],{"class":100},"AsyncSessionLocal ",[83,614,123],{"class":96},[83,616,617],{"class":100}," sessionmaker(engine, ",[83,619,620],{"class":142},"class_",[83,622,123],{"class":96},[83,624,625],{"class":100},"AsyncSession, ",[83,627,628],{"class":142},"expire_on_commit",[83,630,123],{"class":96},[83,632,503],{"class":166},[83,634,261],{"class":100},[83,636,637],{"class":85,"line":157},[83,638,114],{"emptyLinePlaceholder":113},[83,640,641,644,647,651,654,657,660,663],{"class":85,"line":172},[83,642,643],{"class":96},"async",[83,645,646],{"class":96}," def",[83,648,650],{"class":649},"sScJk"," write_order",[83,652,653],{"class":100},"(order_id: ",[83,655,656],{"class":166},"int",[83,658,659],{"class":100},") -> ",[83,661,662],{"class":166},"None",[83,664,665],{"class":100},":\n",[83,667,668,671,674,677,680],{"class":85,"line":181},[83,669,670],{"class":96},"    async",[83,672,673],{"class":96}," with",[83,675,676],{"class":100}," AsyncSessionLocal() ",[83,678,679],{"class":96},"as",[83,681,682],{"class":100}," session:\n",[83,684,685,688,690],{"class":85,"line":194},[83,686,687],{"class":96},"        async",[83,689,673],{"class":96},[83,691,692],{"class":100}," session.begin():\n",[83,694,695,698,701,703,706,709,711,714],{"class":85,"line":207},[83,696,697],{"class":100},"            session.add(Order(",[83,699,700],{"class":142},"id",[83,702,123],{"class":96},[83,704,705],{"class":100},"order_id, ",[83,707,708],{"class":142},"status",[83,710,123],{"class":96},[83,712,713],{"class":132},"\"pending\"",[83,715,716],{"class":100},"))\n",[83,718,719],{"class":85,"line":213},[83,720,114],{"emptyLinePlaceholder":113},[83,722,723],{"class":85,"line":219},[83,724,725],{"class":89},"# These will contend on the single write lock\n",[83,727,728,731,734,737,740,743],{"class":85,"line":232},[83,729,730],{"class":96},"await",[83,732,733],{"class":100}," asyncio.gather(write_order(",[83,735,736],{"class":166},"1",[83,738,739],{"class":100},"), write_order(",[83,741,742],{"class":166},"2",[83,744,716],{"class":100},[14,746,747,748,751],{},"The correct pattern for async SQLite is to use a single shared engine with ",[27,749,750],{},"check_same_thread=False"," and to serialise writes through a queue or simply accept that SQLite is appropriate only for low-concurrency scenarios (tests, local development, single-user tools):",[74,753,755],{"className":76,"code":754,"language":78,"meta":79,"style":79},"# Correct — configure aiosqlite for async use\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"sqlite+aiosqlite:\u002F\u002F\u002Forders.db\",\n    connect_args={\"check_same_thread\": False},\n    # NullPool avoids \"database is locked\" across test isolation boundaries\n    poolclass=StaticPool,  # or NullPool for test isolation\n)\n",[27,756,757,762,772,776,784,791,810,815,828],{"__ignoreMap":79},[83,758,759],{"class":85,"line":86},[83,760,761],{"class":89},"# Correct — configure aiosqlite for async use\n",[83,763,764,766,768,770],{"class":85,"line":93},[83,765,97],{"class":96},[83,767,101],{"class":100},[83,769,104],{"class":96},[83,771,107],{"class":100},[83,773,774],{"class":85,"line":110},[83,775,114],{"emptyLinePlaceholder":113},[83,777,778,780,782],{"class":85,"line":117},[83,779,120],{"class":100},[83,781,123],{"class":96},[83,783,126],{"class":100},[83,785,786,789],{"class":85,"line":129},[83,787,788],{"class":132},"    \"sqlite+aiosqlite:\u002F\u002F\u002Forders.db\"",[83,790,136],{"class":100},[83,792,793,795,797,800,803,805,807],{"class":85,"line":139},[83,794,143],{"class":142},[83,796,123],{"class":96},[83,798,799],{"class":100},"{",[83,801,802],{"class":132},"\"check_same_thread\"",[83,804,163],{"class":100},[83,806,503],{"class":166},[83,808,809],{"class":100},"},\n",[83,811,812],{"class":85,"line":151},[83,813,814],{"class":89},"    # NullPool avoids \"database is locked\" across test isolation boundaries\n",[83,816,817,820,822,825],{"class":85,"line":157},[83,818,819],{"class":142},"    poolclass",[83,821,123],{"class":96},[83,823,824],{"class":100},"StaticPool,  ",[83,826,827],{"class":89},"# or NullPool for test isolation\n",[83,829,830],{"class":85,"line":172},[83,831,261],{"class":100},[14,833,834,835,839],{},"For production multi-writer workloads, SQLite is the wrong database. The ",[18,836,838],{"href":837},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fselecting-async-drivers-for-sqlite-mysql-and-postgres\u002F","guide to selecting async drivers for SQLite, MySQL, and Postgres"," covers when each engine is appropriate.",[277,841,843],{"id":842},"aiomysql-isolation-level-and-autocommit","aiomysql: Isolation Level and Autocommit",[14,845,846,847,850,851,854],{},"MySQL's default transaction isolation level is ",[27,848,849],{},"REPEATABLE READ"," (not ",[27,852,853],{},"READ COMMITTED"," as in PostgreSQL's default). This causes surprising behaviour in long-running async sessions that hold a transaction open while other connections commit: the session may see stale reads because its repeatable-read snapshot was taken at transaction start.",[14,856,857,858,861,862,865],{},"More critically, aiomysql connections start in ",[62,859,860],{},"autocommit=False"," by default, meaning every statement is implicitly wrapped in a transaction. Forgetting to commit or using the session without an explicit ",[27,863,864],{},"begin()"," block leaves uncommitted transactions on the server:",[74,867,869],{"className":76,"code":868,"language":78,"meta":79,"style":79},"# Wrong — implicit transaction never committed\nasync with AsyncSession(engine) as session:\n    result = await session.execute(select(Order).where(Order.status == \"pending\"))\n    orders = result.scalars().all()\n    for order in orders:\n        order.status = \"processing\"\n    # Missing: await session.commit()\n    # MySQL holds an open transaction until the connection returns to the pool\n",[27,870,871,876,889,910,920,934,944,949],{"__ignoreMap":79},[83,872,873],{"class":85,"line":86},[83,874,875],{"class":89},"# Wrong — implicit transaction never committed\n",[83,877,878,880,882,885,887],{"class":85,"line":93},[83,879,643],{"class":96},[83,881,673],{"class":96},[83,883,884],{"class":100}," AsyncSession(engine) ",[83,886,679],{"class":96},[83,888,682],{"class":100},[83,890,891,894,896,899,902,905,908],{"class":85,"line":110},[83,892,893],{"class":100},"    result ",[83,895,123],{"class":96},[83,897,898],{"class":96}," await",[83,900,901],{"class":100}," session.execute(select(Order).where(Order.status ",[83,903,904],{"class":96},"==",[83,906,907],{"class":132}," \"pending\"",[83,909,716],{"class":100},[83,911,912,915,917],{"class":85,"line":117},[83,913,914],{"class":100},"    orders ",[83,916,123],{"class":96},[83,918,919],{"class":100}," result.scalars().all()\n",[83,921,922,925,928,931],{"class":85,"line":129},[83,923,924],{"class":96},"    for",[83,926,927],{"class":100}," order ",[83,929,930],{"class":96},"in",[83,932,933],{"class":100}," orders:\n",[83,935,936,939,941],{"class":85,"line":139},[83,937,938],{"class":100},"        order.status ",[83,940,123],{"class":96},[83,942,943],{"class":132}," \"processing\"\n",[83,945,946],{"class":85,"line":151},[83,947,948],{"class":89},"    # Missing: await session.commit()\n",[83,950,951],{"class":85,"line":157},[83,952,953],{"class":89},"    # MySQL holds an open transaction until the connection returns to the pool\n",[74,955,957],{"className":76,"code":956,"language":78,"meta":79,"style":79},"# Correct — explicit transaction scope\nasync with AsyncSession(engine) as session:\n    async with session.begin():\n        result = await session.execute(select(Order).where(Order.status == \"pending\"))\n        orders = result.scalars().all()\n        for order in orders:\n            order.status = \"processing\"\n    # session.begin() context manager commits on exit\n",[27,958,959,964,976,984,1001,1010,1021,1030],{"__ignoreMap":79},[83,960,961],{"class":85,"line":86},[83,962,963],{"class":89},"# Correct — explicit transaction scope\n",[83,965,966,968,970,972,974],{"class":85,"line":93},[83,967,643],{"class":96},[83,969,673],{"class":96},[83,971,884],{"class":100},[83,973,679],{"class":96},[83,975,682],{"class":100},[83,977,978,980,982],{"class":85,"line":110},[83,979,670],{"class":96},[83,981,673],{"class":96},[83,983,692],{"class":100},[83,985,986,989,991,993,995,997,999],{"class":85,"line":117},[83,987,988],{"class":100},"        result ",[83,990,123],{"class":96},[83,992,898],{"class":96},[83,994,901],{"class":100},[83,996,904],{"class":96},[83,998,907],{"class":132},[83,1000,716],{"class":100},[83,1002,1003,1006,1008],{"class":85,"line":129},[83,1004,1005],{"class":100},"        orders ",[83,1007,123],{"class":96},[83,1009,919],{"class":100},[83,1011,1012,1015,1017,1019],{"class":85,"line":139},[83,1013,1014],{"class":96},"        for",[83,1016,927],{"class":100},[83,1018,930],{"class":96},[83,1020,933],{"class":100},[83,1022,1023,1026,1028],{"class":85,"line":151},[83,1024,1025],{"class":100},"            order.status ",[83,1027,123],{"class":96},[83,1029,943],{"class":132},[83,1031,1032],{"class":85,"line":157},[83,1033,1034],{"class":89},"    # session.begin() context manager commits on exit\n",[14,1036,1037],{},"To change the isolation level for aiomysql:",[74,1039,1041],{"className":76,"code":1040,"language":78,"meta":79,"style":79},"from sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"mysql+aiomysql:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    isolation_level=\"READ COMMITTED\",  # match Postgres behaviour\n    connect_args={\n        \"charset\": \"utf8mb4\",  # always set explicitly\n        \"autocommit\": False,\n    },\n)\n",[27,1042,1043,1053,1057,1065,1072,1087,1095,1110,1121,1125],{"__ignoreMap":79},[83,1044,1045,1047,1049,1051],{"class":85,"line":86},[83,1046,97],{"class":96},[83,1048,101],{"class":100},[83,1050,104],{"class":96},[83,1052,107],{"class":100},[83,1054,1055],{"class":85,"line":93},[83,1056,114],{"emptyLinePlaceholder":113},[83,1058,1059,1061,1063],{"class":85,"line":110},[83,1060,120],{"class":100},[83,1062,123],{"class":96},[83,1064,126],{"class":100},[83,1066,1067,1070],{"class":85,"line":117},[83,1068,1069],{"class":132},"    \"mysql+aiomysql:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[83,1071,136],{"class":100},[83,1073,1074,1077,1079,1082,1084],{"class":85,"line":129},[83,1075,1076],{"class":142},"    isolation_level",[83,1078,123],{"class":96},[83,1080,1081],{"class":132},"\"READ COMMITTED\"",[83,1083,382],{"class":100},[83,1085,1086],{"class":89},"# match Postgres behaviour\n",[83,1088,1089,1091,1093],{"class":85,"line":139},[83,1090,143],{"class":142},[83,1092,123],{"class":96},[83,1094,148],{"class":100},[83,1096,1097,1100,1102,1105,1107],{"class":85,"line":151},[83,1098,1099],{"class":132},"        \"charset\"",[83,1101,163],{"class":100},[83,1103,1104],{"class":132},"\"utf8mb4\"",[83,1106,382],{"class":100},[83,1108,1109],{"class":89},"# always set explicitly\n",[83,1111,1112,1115,1117,1119],{"class":85,"line":157},[83,1113,1114],{"class":132},"        \"autocommit\"",[83,1116,163],{"class":100},[83,1118,503],{"class":166},[83,1120,136],{"class":100},[83,1122,1123],{"class":85,"line":172},[83,1124,216],{"class":100},[83,1126,1127],{"class":85,"line":181},[83,1128,261],{"class":100},[36,1130,1132],{"id":1131},"advanced-driver-specific-patterns","Advanced Driver-Specific Patterns",[277,1134,1136],{"id":1135},"asyncpg-type-codec-quirks","asyncpg Type Codec Quirks",[14,1138,1139,1140,1143],{},"asyncpg ships its own binary type encoding system, independent of PostgreSQL's ",[27,1141,1142],{},"libpq"," text protocol. This means some Python types map to PostgreSQL types differently than you might expect:",[14,1145,1146,1149,1150,1153,1154,1157,1158,1161,1162,1165,1166,1169,1170,1173],{},[62,1147,1148],{},"JSON and JSONB",": asyncpg decodes both ",[27,1151,1152],{},"json"," and ",[27,1155,1156],{},"jsonb"," columns as Python ",[27,1159,1160],{},"str"," by default, not as ",[27,1163,1164],{},"dict",". SQLAlchemy's ",[27,1167,1168],{},"JSON"," column type works around this, but raw ",[27,1171,1172],{},"text()"," queries against JSONB columns return strings:",[74,1175,1177],{"className":76,"code":1176,"language":78,"meta":79,"style":79},"# asyncpg returns str for raw JSON queries — must decode manually\nfrom sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nasync def get_tenant_config(session: AsyncSession, tenant_id: int) -> dict:\n    result = await session.execute(\n        text(\"SELECT config FROM tenants WHERE id = :id\"),\n        {\"id\": tenant_id},\n    )\n    row = result.fetchone()\n    if row is None:\n        return {}\n    # row.config is a str when column is json\u002Fjsonb in raw text()\n    import json\n    return json.loads(row.config) if isinstance(row.config, str) else row.config\n",[27,1178,1179,1184,1196,1207,1211,1231,1242,1253,1264,1269,1279,1295,1303,1308,1316],{"__ignoreMap":79},[83,1180,1181],{"class":85,"line":86},[83,1182,1183],{"class":89},"# asyncpg returns str for raw JSON queries — must decode manually\n",[83,1185,1186,1188,1191,1193],{"class":85,"line":93},[83,1187,97],{"class":96},[83,1189,1190],{"class":100}," sqlalchemy ",[83,1192,104],{"class":96},[83,1194,1195],{"class":100}," text\n",[83,1197,1198,1200,1202,1204],{"class":85,"line":110},[83,1199,97],{"class":96},[83,1201,101],{"class":100},[83,1203,104],{"class":96},[83,1205,1206],{"class":100}," AsyncSession\n",[83,1208,1209],{"class":85,"line":117},[83,1210,114],{"emptyLinePlaceholder":113},[83,1212,1213,1215,1217,1220,1223,1225,1227,1229],{"class":85,"line":129},[83,1214,643],{"class":96},[83,1216,646],{"class":96},[83,1218,1219],{"class":649}," get_tenant_config",[83,1221,1222],{"class":100},"(session: AsyncSession, tenant_id: ",[83,1224,656],{"class":166},[83,1226,659],{"class":100},[83,1228,1164],{"class":166},[83,1230,665],{"class":100},[83,1232,1233,1235,1237,1239],{"class":85,"line":139},[83,1234,893],{"class":100},[83,1236,123],{"class":96},[83,1238,898],{"class":96},[83,1240,1241],{"class":100}," session.execute(\n",[83,1243,1244,1247,1250],{"class":85,"line":151},[83,1245,1246],{"class":100},"        text(",[83,1248,1249],{"class":132},"\"SELECT config FROM tenants WHERE id = :id\"",[83,1251,1252],{"class":100},"),\n",[83,1254,1255,1258,1261],{"class":85,"line":157},[83,1256,1257],{"class":100},"        {",[83,1259,1260],{"class":132},"\"id\"",[83,1262,1263],{"class":100},": tenant_id},\n",[83,1265,1266],{"class":85,"line":172},[83,1267,1268],{"class":100},"    )\n",[83,1270,1271,1274,1276],{"class":85,"line":181},[83,1272,1273],{"class":100},"    row ",[83,1275,123],{"class":96},[83,1277,1278],{"class":100}," result.fetchone()\n",[83,1280,1281,1284,1287,1290,1293],{"class":85,"line":194},[83,1282,1283],{"class":96},"    if",[83,1285,1286],{"class":100}," row ",[83,1288,1289],{"class":96},"is",[83,1291,1292],{"class":166}," None",[83,1294,665],{"class":100},[83,1296,1297,1300],{"class":85,"line":207},[83,1298,1299],{"class":96},"        return",[83,1301,1302],{"class":100}," {}\n",[83,1304,1305],{"class":85,"line":213},[83,1306,1307],{"class":89},"    # row.config is a str when column is json\u002Fjsonb in raw text()\n",[83,1309,1310,1313],{"class":85,"line":219},[83,1311,1312],{"class":96},"    import",[83,1314,1315],{"class":100}," json\n",[83,1317,1318,1321,1324,1327,1330,1333,1335,1338,1341],{"class":85,"line":232},[83,1319,1320],{"class":96},"    return",[83,1322,1323],{"class":100}," json.loads(row.config) ",[83,1325,1326],{"class":96},"if",[83,1328,1329],{"class":166}," isinstance",[83,1331,1332],{"class":100},"(row.config, ",[83,1334,1160],{"class":166},[83,1336,1337],{"class":100},") ",[83,1339,1340],{"class":96},"else",[83,1342,1343],{"class":100}," row.config\n",[14,1345,1346,1347,1349],{},"Use SQLAlchemy's ",[27,1348,1168],{}," type on the mapped column to ensure automatic codec registration:",[74,1351,1353],{"className":76,"code":1352,"language":78,"meta":79,"style":79},"from sqlalchemy import Column, Integer, JSON\nfrom sqlalchemy.orm import DeclarativeBase\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Tenant(Base):\n    __tablename__ = \"tenants\"\n    id = Column(Integer, primary_key=True)\n    config = Column(JSON)  # SQLAlchemy registers asyncpg codec automatically\n",[27,1354,1355,1369,1380,1384,1401,1406,1410,1424,1434,1454],{"__ignoreMap":79},[83,1356,1357,1359,1361,1363,1366],{"class":85,"line":86},[83,1358,97],{"class":96},[83,1360,1190],{"class":100},[83,1362,104],{"class":96},[83,1364,1365],{"class":100}," Column, Integer, ",[83,1367,1368],{"class":166},"JSON\n",[83,1370,1371,1373,1375,1377],{"class":85,"line":93},[83,1372,97],{"class":96},[83,1374,584],{"class":100},[83,1376,104],{"class":96},[83,1378,1379],{"class":100}," DeclarativeBase\n",[83,1381,1382],{"class":85,"line":110},[83,1383,114],{"emptyLinePlaceholder":113},[83,1385,1386,1389,1392,1395,1398],{"class":85,"line":117},[83,1387,1388],{"class":96},"class",[83,1390,1391],{"class":649}," Base",[83,1393,1394],{"class":100},"(",[83,1396,1397],{"class":649},"DeclarativeBase",[83,1399,1400],{"class":100},"):\n",[83,1402,1403],{"class":85,"line":129},[83,1404,1405],{"class":96},"    pass\n",[83,1407,1408],{"class":85,"line":139},[83,1409,114],{"emptyLinePlaceholder":113},[83,1411,1412,1414,1417,1419,1422],{"class":85,"line":151},[83,1413,1388],{"class":96},[83,1415,1416],{"class":649}," Tenant",[83,1418,1394],{"class":100},[83,1420,1421],{"class":649},"Base",[83,1423,1400],{"class":100},[83,1425,1426,1429,1431],{"class":85,"line":157},[83,1427,1428],{"class":100},"    __tablename__ ",[83,1430,123],{"class":96},[83,1432,1433],{"class":132}," \"tenants\"\n",[83,1435,1436,1439,1442,1445,1448,1450,1452],{"class":85,"line":172},[83,1437,1438],{"class":166},"    id",[83,1440,1441],{"class":96}," =",[83,1443,1444],{"class":100}," Column(Integer, ",[83,1446,1447],{"class":142},"primary_key",[83,1449,123],{"class":96},[83,1451,253],{"class":166},[83,1453,261],{"class":100},[83,1455,1456,1459,1461,1464,1466,1469],{"class":85,"line":181},[83,1457,1458],{"class":100},"    config ",[83,1460,123],{"class":96},[83,1462,1463],{"class":100}," Column(",[83,1465,1168],{"class":166},[83,1467,1468],{"class":100},")  ",[83,1470,1471],{"class":89},"# SQLAlchemy registers asyncpg codec automatically\n",[14,1473,1474,1477,1478,1157,1481,1484,1485,1487,1488,1491],{},[62,1475,1476],{},"NUMERIC and Decimal",": asyncpg decodes ",[27,1479,1480],{},"NUMERIC",[27,1482,1483],{},"Decimal"," by default. However, when using ",[27,1486,1172],{}," with inline arithmetic, the result type may be inferred as ",[27,1489,1490],{},"float"," by asyncpg's codec:",[74,1493,1495],{"className":76,"code":1494,"language":78,"meta":79,"style":79},"# asyncpg may return float for computed numeric expressions\nresult = await session.execute(\n    text(\"SELECT SUM(amount) * 1.1 AS total FROM invoices WHERE tenant_id = :id\"),\n    {\"id\": tenant_id},\n)\n# total may be float, not Decimal — use explicit CAST in SQL\nresult = await session.execute(\n    text(\"SELECT CAST(SUM(amount) * 1.1 AS NUMERIC(12,2)) AS total FROM invoices WHERE tenant_id = :id\"),\n    {\"id\": tenant_id},\n)\n",[27,1496,1497,1502,1513,1523,1532,1536,1541,1551,1560,1568],{"__ignoreMap":79},[83,1498,1499],{"class":85,"line":86},[83,1500,1501],{"class":89},"# asyncpg may return float for computed numeric expressions\n",[83,1503,1504,1507,1509,1511],{"class":85,"line":93},[83,1505,1506],{"class":100},"result ",[83,1508,123],{"class":96},[83,1510,898],{"class":96},[83,1512,1241],{"class":100},[83,1514,1515,1518,1521],{"class":85,"line":110},[83,1516,1517],{"class":100},"    text(",[83,1519,1520],{"class":132},"\"SELECT SUM(amount) * 1.1 AS total FROM invoices WHERE tenant_id = :id\"",[83,1522,1252],{"class":100},[83,1524,1525,1528,1530],{"class":85,"line":117},[83,1526,1527],{"class":100},"    {",[83,1529,1260],{"class":132},[83,1531,1263],{"class":100},[83,1533,1534],{"class":85,"line":129},[83,1535,261],{"class":100},[83,1537,1538],{"class":85,"line":139},[83,1539,1540],{"class":89},"# total may be float, not Decimal — use explicit CAST in SQL\n",[83,1542,1543,1545,1547,1549],{"class":85,"line":151},[83,1544,1506],{"class":100},[83,1546,123],{"class":96},[83,1548,898],{"class":96},[83,1550,1241],{"class":100},[83,1552,1553,1555,1558],{"class":85,"line":157},[83,1554,1517],{"class":100},[83,1556,1557],{"class":132},"\"SELECT CAST(SUM(amount) * 1.1 AS NUMERIC(12,2)) AS total FROM invoices WHERE tenant_id = :id\"",[83,1559,1252],{"class":100},[83,1561,1562,1564,1566],{"class":85,"line":172},[83,1563,1527],{"class":100},[83,1565,1260],{"class":132},[83,1567,1263],{"class":100},[83,1569,1570],{"class":85,"line":181},[83,1571,261],{"class":100},[14,1573,1574,1577,1578,1581,1582,1585,1586,1588,1589,1591],{},[62,1575,1576],{},"Enum types",": PostgreSQL ",[27,1579,1580],{},"ENUM"," types require asyncpg to register a codec for each custom enum. SQLAlchemy's ",[27,1583,1584],{},"Enum"," column type handles this automatically for mapped columns, but ad-hoc ",[27,1587,1172],{}," queries against enum columns return ",[27,1590,1160],{}," values:",[74,1593,1595],{"className":76,"code":1594,"language":78,"meta":79,"style":79},"# Create PostgreSQL enum with SQLAlchemy — codec registered automatically\nfrom sqlalchemy import Enum as SAEnum\n\nclass OrderStatus(enum.Enum):\n    PENDING = \"pending\"\n    PROCESSING = \"processing\"\n    SHIPPED = \"shipped\"\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id = Column(Integer, primary_key=True)\n    status = Column(SAEnum(OrderStatus, name=\"order_status_enum\"))\n",[27,1596,1597,1602,1618,1622,1640,1650,1659,1669,1673,1686,1695,1711],{"__ignoreMap":79},[83,1598,1599],{"class":85,"line":86},[83,1600,1601],{"class":89},"# Create PostgreSQL enum with SQLAlchemy — codec registered automatically\n",[83,1603,1604,1606,1608,1610,1613,1615],{"class":85,"line":93},[83,1605,97],{"class":96},[83,1607,1190],{"class":100},[83,1609,104],{"class":96},[83,1611,1612],{"class":100}," Enum ",[83,1614,679],{"class":96},[83,1616,1617],{"class":100}," SAEnum\n",[83,1619,1620],{"class":85,"line":110},[83,1621,114],{"emptyLinePlaceholder":113},[83,1623,1624,1626,1629,1631,1634,1636,1638],{"class":85,"line":117},[83,1625,1388],{"class":96},[83,1627,1628],{"class":649}," OrderStatus",[83,1630,1394],{"class":100},[83,1632,1633],{"class":649},"enum",[83,1635,22],{"class":100},[83,1637,1584],{"class":649},[83,1639,1400],{"class":100},[83,1641,1642,1645,1647],{"class":85,"line":129},[83,1643,1644],{"class":166},"    PENDING",[83,1646,1441],{"class":96},[83,1648,1649],{"class":132}," \"pending\"\n",[83,1651,1652,1655,1657],{"class":85,"line":139},[83,1653,1654],{"class":166},"    PROCESSING",[83,1656,1441],{"class":96},[83,1658,943],{"class":132},[83,1660,1661,1664,1666],{"class":85,"line":151},[83,1662,1663],{"class":166},"    SHIPPED",[83,1665,1441],{"class":96},[83,1667,1668],{"class":132}," \"shipped\"\n",[83,1670,1671],{"class":85,"line":157},[83,1672,114],{"emptyLinePlaceholder":113},[83,1674,1675,1677,1680,1682,1684],{"class":85,"line":172},[83,1676,1388],{"class":96},[83,1678,1679],{"class":649}," Order",[83,1681,1394],{"class":100},[83,1683,1421],{"class":649},[83,1685,1400],{"class":100},[83,1687,1688,1690,1692],{"class":85,"line":181},[83,1689,1428],{"class":100},[83,1691,123],{"class":96},[83,1693,1694],{"class":132}," \"orders\"\n",[83,1696,1697,1699,1701,1703,1705,1707,1709],{"class":85,"line":194},[83,1698,1438],{"class":166},[83,1700,1441],{"class":96},[83,1702,1444],{"class":100},[83,1704,1447],{"class":142},[83,1706,123],{"class":96},[83,1708,253],{"class":166},[83,1710,261],{"class":100},[83,1712,1713,1716,1718,1721,1724,1726,1729],{"class":85,"line":207},[83,1714,1715],{"class":100},"    status ",[83,1717,123],{"class":96},[83,1719,1720],{"class":100}," Column(SAEnum(OrderStatus, ",[83,1722,1723],{"class":142},"name",[83,1725,123],{"class":96},[83,1727,1728],{"class":132},"\"order_status_enum\"",[83,1730,716],{"class":100},[277,1732,1734],{"id":1733},"server_settings-via-connect_args","server_settings via connect_args",[14,1736,1737,1738,1741,1742,1745],{},"asyncpg (and psycopg3) support passing PostgreSQL ",[27,1739,1740],{},"SET"," configuration parameters at connection establishment time via ",[27,1743,1744],{},"server_settings",". This is the correct place to configure parameters that must be set per-connection rather than globally:",[74,1747,1749],{"className":76,"code":1748,"language":78,"meta":79,"style":79},"from sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    connect_args={\n        \"server_settings\": {\n            \"application_name\": \"order_service_v2\",\n            \"statement_timeout\": \"30000\",       # ms — kill runaway queries\n            \"lock_timeout\": \"5000\",             # ms — avoid deadlock waits\n            \"idle_in_transaction_session_timeout\": \"60000\",  # ms — release stuck sessions\n            \"search_path\": \"myschema,public\",   # schema routing\n            \"jit\": \"off\",                       # disable JIT for OLTP workloads\n        },\n    },\n)\n",[27,1750,1751,1761,1765,1773,1779,1787,1793,1804,1820,1836,1851,1867,1881,1885,1889],{"__ignoreMap":79},[83,1752,1753,1755,1757,1759],{"class":85,"line":86},[83,1754,97],{"class":96},[83,1756,101],{"class":100},[83,1758,104],{"class":96},[83,1760,107],{"class":100},[83,1762,1763],{"class":85,"line":93},[83,1764,114],{"emptyLinePlaceholder":113},[83,1766,1767,1769,1771],{"class":85,"line":110},[83,1768,120],{"class":100},[83,1770,123],{"class":96},[83,1772,126],{"class":100},[83,1774,1775,1777],{"class":85,"line":117},[83,1776,133],{"class":132},[83,1778,136],{"class":100},[83,1780,1781,1783,1785],{"class":85,"line":129},[83,1782,143],{"class":142},[83,1784,123],{"class":96},[83,1786,148],{"class":100},[83,1788,1789,1791],{"class":85,"line":139},[83,1790,175],{"class":132},[83,1792,178],{"class":100},[83,1794,1795,1797,1799,1802],{"class":85,"line":151},[83,1796,184],{"class":132},[83,1798,163],{"class":100},[83,1800,1801],{"class":132},"\"order_service_v2\"",[83,1803,136],{"class":100},[83,1805,1806,1809,1811,1814,1817],{"class":85,"line":157},[83,1807,1808],{"class":132},"            \"statement_timeout\"",[83,1810,163],{"class":100},[83,1812,1813],{"class":132},"\"30000\"",[83,1815,1816],{"class":100},",       ",[83,1818,1819],{"class":89},"# ms — kill runaway queries\n",[83,1821,1822,1825,1827,1830,1833],{"class":85,"line":172},[83,1823,1824],{"class":132},"            \"lock_timeout\"",[83,1826,163],{"class":100},[83,1828,1829],{"class":132},"\"5000\"",[83,1831,1832],{"class":100},",             ",[83,1834,1835],{"class":89},"# ms — avoid deadlock waits\n",[83,1837,1838,1841,1843,1846,1848],{"class":85,"line":181},[83,1839,1840],{"class":132},"            \"idle_in_transaction_session_timeout\"",[83,1842,163],{"class":100},[83,1844,1845],{"class":132},"\"60000\"",[83,1847,382],{"class":100},[83,1849,1850],{"class":89},"# ms — release stuck sessions\n",[83,1852,1853,1856,1858,1861,1864],{"class":85,"line":194},[83,1854,1855],{"class":132},"            \"search_path\"",[83,1857,163],{"class":100},[83,1859,1860],{"class":132},"\"myschema,public\"",[83,1862,1863],{"class":100},",   ",[83,1865,1866],{"class":89},"# schema routing\n",[83,1868,1869,1871,1873,1875,1878],{"class":85,"line":207},[83,1870,197],{"class":132},[83,1872,163],{"class":100},[83,1874,202],{"class":132},[83,1876,1877],{"class":100},",                       ",[83,1879,1880],{"class":89},"# disable JIT for OLTP workloads\n",[83,1882,1883],{"class":85,"line":213},[83,1884,210],{"class":100},[83,1886,1887],{"class":85,"line":219},[83,1888,216],{"class":100},[83,1890,1891],{"class":85,"line":232},[83,1892,261],{"class":100},[14,1894,1895,1896,1899],{},"These are equivalent to ",[27,1897,1898],{},"ALTER ROLE ... SET parameter = value"," but scoped to the current connection, making them safe for multi-tenant engines with different per-tenant schemas.",[277,1901,1903],{"id":1902},"returning-clause-support","RETURNING Clause Support",[14,1905,1906,1909,1910,1153,1913,1916],{},[27,1907,1908],{},"RETURNING"," is a PostgreSQL extension; SQLAlchemy generates it for ",[27,1911,1912],{},"insert().returning()",[27,1914,1915],{},"update().returning()"," statements on PostgreSQL dialects, but the behaviour varies:",[1918,1919,1920,1930,1950],"ul",{},[1921,1922,1923,1926,1927,1929],"li",{},[62,1924,1925],{},"asyncpg \u002F psycopg3 (PostgreSQL)",": Full ",[27,1928,1908],{}," support, including multi-row returns from bulk inserts.",[1921,1931,1932,1935,1936,1938,1939,1942,1943,1946,1947,22],{},[62,1933,1934],{},"aiomysql (MySQL 8.0+)",": No ",[27,1937,1908],{}," support. SQLAlchemy falls back to ",[27,1940,1941],{},"INSERT ... ; SELECT LAST_INSERT_ID()"," for single-row inserts. Bulk inserts with ",[27,1944,1945],{},"returning()"," raise ",[27,1948,1949],{},"CompileError",[1921,1951,1952,163,1955,1957,1958,22],{},[62,1953,1954],{},"aiosqlite (SQLite 3.35+)",[27,1956,1908],{}," supported for SQLite ≥ 3.35 only. Check your SQLite version: older versions raise ",[27,1959,1960],{},"OperationalError: near \"RETURNING\": syntax error",[74,1962,1964],{"className":76,"code":1963,"language":78,"meta":79,"style":79},"# Portable insert with RETURNING — works on asyncpg\u002Fpsycopg3 only\nfrom sqlalchemy import insert\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nasync def create_invoice(session: AsyncSession, data: dict) -> int:\n    stmt = (\n        insert(Invoice)\n        .values(**data)\n        .returning(Invoice.id)\n    )\n    result = await session.execute(stmt)\n    return result.scalar_one()\n",[27,1965,1966,1971,1982,1992,1996,2016,2026,2031,2042,2047,2051,2062],{"__ignoreMap":79},[83,1967,1968],{"class":85,"line":86},[83,1969,1970],{"class":89},"# Portable insert with RETURNING — works on asyncpg\u002Fpsycopg3 only\n",[83,1972,1973,1975,1977,1979],{"class":85,"line":93},[83,1974,97],{"class":96},[83,1976,1190],{"class":100},[83,1978,104],{"class":96},[83,1980,1981],{"class":100}," insert\n",[83,1983,1984,1986,1988,1990],{"class":85,"line":110},[83,1985,97],{"class":96},[83,1987,101],{"class":100},[83,1989,104],{"class":96},[83,1991,1206],{"class":100},[83,1993,1994],{"class":85,"line":117},[83,1995,114],{"emptyLinePlaceholder":113},[83,1997,1998,2000,2002,2005,2008,2010,2012,2014],{"class":85,"line":129},[83,1999,643],{"class":96},[83,2001,646],{"class":96},[83,2003,2004],{"class":649}," create_invoice",[83,2006,2007],{"class":100},"(session: AsyncSession, data: ",[83,2009,1164],{"class":166},[83,2011,659],{"class":100},[83,2013,656],{"class":166},[83,2015,665],{"class":100},[83,2017,2018,2021,2023],{"class":85,"line":139},[83,2019,2020],{"class":100},"    stmt ",[83,2022,123],{"class":96},[83,2024,2025],{"class":100}," (\n",[83,2027,2028],{"class":85,"line":151},[83,2029,2030],{"class":100},"        insert(Invoice)\n",[83,2032,2033,2036,2039],{"class":85,"line":157},[83,2034,2035],{"class":100},"        .values(",[83,2037,2038],{"class":96},"**",[83,2040,2041],{"class":100},"data)\n",[83,2043,2044],{"class":85,"line":172},[83,2045,2046],{"class":100},"        .returning(Invoice.id)\n",[83,2048,2049],{"class":85,"line":181},[83,2050,1268],{"class":100},[83,2052,2053,2055,2057,2059],{"class":85,"line":194},[83,2054,893],{"class":100},[83,2056,123],{"class":96},[83,2058,898],{"class":96},[83,2060,2061],{"class":100}," session.execute(stmt)\n",[83,2063,2064,2066],{"class":85,"line":207},[83,2065,1320],{"class":96},[83,2067,2068],{"class":100}," result.scalar_one()\n",[14,2070,2071,2072,2075,2076,2079],{},"For MySQL and old SQLite, the equivalent pattern uses ",[27,2073,2074],{},"session.add()"," + ",[27,2077,2078],{},"await session.flush()",":",[74,2081,2083],{"className":76,"code":2082,"language":78,"meta":79,"style":79},"# Portable pattern — works across all dialects\nasync def create_invoice_portable(session: AsyncSession, data: dict) -> Invoice:\n    invoice = Invoice(**data)\n    session.add(invoice)\n    await session.flush()   # populates invoice.id via post-insert SELECT or LAST_INSERT_ID()\n    await session.refresh(invoice)\n    return invoice\n",[27,2084,2085,2090,2106,2120,2125,2136,2143],{"__ignoreMap":79},[83,2086,2087],{"class":85,"line":86},[83,2088,2089],{"class":89},"# Portable pattern — works across all dialects\n",[83,2091,2092,2094,2096,2099,2101,2103],{"class":85,"line":93},[83,2093,643],{"class":96},[83,2095,646],{"class":96},[83,2097,2098],{"class":649}," create_invoice_portable",[83,2100,2007],{"class":100},[83,2102,1164],{"class":166},[83,2104,2105],{"class":100},") -> Invoice:\n",[83,2107,2108,2111,2113,2116,2118],{"class":85,"line":110},[83,2109,2110],{"class":100},"    invoice ",[83,2112,123],{"class":96},[83,2114,2115],{"class":100}," Invoice(",[83,2117,2038],{"class":96},[83,2119,2041],{"class":100},[83,2121,2122],{"class":85,"line":117},[83,2123,2124],{"class":100},"    session.add(invoice)\n",[83,2126,2127,2130,2133],{"class":85,"line":129},[83,2128,2129],{"class":96},"    await",[83,2131,2132],{"class":100}," session.flush()   ",[83,2134,2135],{"class":89},"# populates invoice.id via post-insert SELECT or LAST_INSERT_ID()\n",[83,2137,2138,2140],{"class":85,"line":139},[83,2139,2129],{"class":96},[83,2141,2142],{"class":100}," session.refresh(invoice)\n",[83,2144,2145,2147],{"class":85,"line":151},[83,2146,1320],{"class":96},[83,2148,2149],{"class":100}," invoice\n",[277,2151,2153],{"id":2152},"named-vs-positional-parameters","Named vs Positional Parameters",[14,2155,2156,2157,2160,2161,2164,2165,2168,2169,2079],{},"asyncpg uses positional parameters (",[27,2158,2159],{},"$1",", ",[27,2162,2163],{},"$2",", ...) internally. SQLAlchemy translates its named parameters (",[27,2166,2167],{},":param",") to positional at compile time, so you never see this directly — unless you drop to raw asyncpg via ",[27,2170,2171],{},"engine.raw_connection()",[74,2173,2175],{"className":76,"code":2174,"language":78,"meta":79,"style":79},"# Wrong — SQLAlchemy named params, but using raw asyncpg connection\nconn = await engine.raw_connection()\ntry:\n    # asyncpg requires positional $1 params, not :name\n    await conn.fetchrow(\"SELECT * FROM orders WHERE id = :id\", order_id)  # raises\nfinally:\n    conn.close()\n\n# Correct — use positional params with raw asyncpg\nconn = await engine.raw_connection()\ntry:\n    row = await conn.fetchrow(\"SELECT * FROM orders WHERE id = $1\", order_id)\nfinally:\n    conn.close()\n",[27,2176,2177,2182,2194,2201,2206,2222,2229,2234,2238,2243,2253,2259,2275,2281],{"__ignoreMap":79},[83,2178,2179],{"class":85,"line":86},[83,2180,2181],{"class":89},"# Wrong — SQLAlchemy named params, but using raw asyncpg connection\n",[83,2183,2184,2187,2189,2191],{"class":85,"line":93},[83,2185,2186],{"class":100},"conn ",[83,2188,123],{"class":96},[83,2190,898],{"class":96},[83,2192,2193],{"class":100}," engine.raw_connection()\n",[83,2195,2196,2199],{"class":85,"line":110},[83,2197,2198],{"class":96},"try",[83,2200,665],{"class":100},[83,2202,2203],{"class":85,"line":117},[83,2204,2205],{"class":89},"    # asyncpg requires positional $1 params, not :name\n",[83,2207,2208,2210,2213,2216,2219],{"class":85,"line":129},[83,2209,2129],{"class":96},[83,2211,2212],{"class":100}," conn.fetchrow(",[83,2214,2215],{"class":132},"\"SELECT * FROM orders WHERE id = :id\"",[83,2217,2218],{"class":100},", order_id)  ",[83,2220,2221],{"class":89},"# raises\n",[83,2223,2224,2227],{"class":85,"line":139},[83,2225,2226],{"class":96},"finally",[83,2228,665],{"class":100},[83,2230,2231],{"class":85,"line":151},[83,2232,2233],{"class":100},"    conn.close()\n",[83,2235,2236],{"class":85,"line":157},[83,2237,114],{"emptyLinePlaceholder":113},[83,2239,2240],{"class":85,"line":172},[83,2241,2242],{"class":89},"# Correct — use positional params with raw asyncpg\n",[83,2244,2245,2247,2249,2251],{"class":85,"line":181},[83,2246,2186],{"class":100},[83,2248,123],{"class":96},[83,2250,898],{"class":96},[83,2252,2193],{"class":100},[83,2254,2255,2257],{"class":85,"line":194},[83,2256,2198],{"class":96},[83,2258,665],{"class":100},[83,2260,2261,2263,2265,2267,2269,2272],{"class":85,"line":207},[83,2262,1273],{"class":100},[83,2264,123],{"class":96},[83,2266,898],{"class":96},[83,2268,2212],{"class":100},[83,2270,2271],{"class":132},"\"SELECT * FROM orders WHERE id = $1\"",[83,2273,2274],{"class":100},", order_id)\n",[83,2276,2277,2279],{"class":85,"line":213},[83,2278,2226],{"class":96},[83,2280,665],{"class":100},[83,2282,2283],{"class":85,"line":219},[83,2284,2233],{"class":100},[14,2286,2287,2288,2290,2291,2294],{},"When using ",[27,2289,1172],{}," through SQLAlchemy's session, always use ",[27,2292,2293],{},":name"," style — SQLAlchemy's compiler translates to the driver's native param style:",[74,2296,2298],{"className":76,"code":2297,"language":78,"meta":79,"style":79},"# Correct — SQLAlchemy handles translation\nresult = await session.execute(\n    text(\"SELECT * FROM orders WHERE tenant_id = :tid AND status = :status\"),\n    {\"tid\": tenant_id, \"status\": \"pending\"},\n)\n",[27,2299,2300,2305,2315,2324,2343],{"__ignoreMap":79},[83,2301,2302],{"class":85,"line":86},[83,2303,2304],{"class":89},"# Correct — SQLAlchemy handles translation\n",[83,2306,2307,2309,2311,2313],{"class":85,"line":93},[83,2308,1506],{"class":100},[83,2310,123],{"class":96},[83,2312,898],{"class":96},[83,2314,1241],{"class":100},[83,2316,2317,2319,2322],{"class":85,"line":110},[83,2318,1517],{"class":100},[83,2320,2321],{"class":132},"\"SELECT * FROM orders WHERE tenant_id = :tid AND status = :status\"",[83,2323,1252],{"class":100},[83,2325,2326,2328,2331,2334,2337,2339,2341],{"class":85,"line":117},[83,2327,1527],{"class":100},[83,2329,2330],{"class":132},"\"tid\"",[83,2332,2333],{"class":100},": tenant_id, ",[83,2335,2336],{"class":132},"\"status\"",[83,2338,163],{"class":100},[83,2340,713],{"class":132},[83,2342,809],{"class":100},[83,2344,2345],{"class":85,"line":129},[83,2346,261],{"class":100},[36,2348,2350],{"id":2349},"hybrid-architectures-migration-strategies","Hybrid Architectures & Migration Strategies",[277,2352,2354],{"id":2353},"mixing-sync-and-async-engines-in-the-same-process","Mixing Sync and Async Engines in the Same Process",[14,2356,2357],{},"Some applications need both sync and async database access — for example, Celery workers (sync) alongside FastAPI handlers (async) sharing the same SQLAlchemy models. The correct approach is to create separate engines rather than attempting to share a single engine:",[74,2359,2361],{"className":76,"code":2360,"language":78,"meta":79,"style":79},"# Two engines, same models — sync for Celery, async for FastAPI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\n# Sync engine for Celery workers\nsync_engine = create_engine(\n    \"postgresql+psycopg2:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    pool_size=5,\n    max_overflow=10,\n)\n\n# Async engine for FastAPI\nasync_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    pool_size=20,\n    max_overflow=40,\n    connect_args={\"statement_cache_size\": 0},  # if behind PgBouncer\n)\n",[27,2362,2363,2368,2379,2389,2393,2398,2408,2415,2426,2436,2440,2444,2449,2458,2464,2474,2485,2506],{"__ignoreMap":79},[83,2364,2365],{"class":85,"line":86},[83,2366,2367],{"class":89},"# Two engines, same models — sync for Celery, async for FastAPI\n",[83,2369,2370,2372,2374,2376],{"class":85,"line":93},[83,2371,97],{"class":96},[83,2373,1190],{"class":100},[83,2375,104],{"class":96},[83,2377,2378],{"class":100}," create_engine\n",[83,2380,2381,2383,2385,2387],{"class":85,"line":110},[83,2382,97],{"class":96},[83,2384,101],{"class":100},[83,2386,104],{"class":96},[83,2388,107],{"class":100},[83,2390,2391],{"class":85,"line":117},[83,2392,114],{"emptyLinePlaceholder":113},[83,2394,2395],{"class":85,"line":129},[83,2396,2397],{"class":89},"# Sync engine for Celery workers\n",[83,2399,2400,2403,2405],{"class":85,"line":139},[83,2401,2402],{"class":100},"sync_engine ",[83,2404,123],{"class":96},[83,2406,2407],{"class":100}," create_engine(\n",[83,2409,2410,2413],{"class":85,"line":151},[83,2411,2412],{"class":132},"    \"postgresql+psycopg2:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[83,2414,136],{"class":100},[83,2416,2417,2419,2421,2424],{"class":85,"line":157},[83,2418,222],{"class":142},[83,2420,123],{"class":96},[83,2422,2423],{"class":166},"5",[83,2425,136],{"class":100},[83,2427,2428,2430,2432,2434],{"class":85,"line":172},[83,2429,235],{"class":142},[83,2431,123],{"class":96},[83,2433,227],{"class":166},[83,2435,136],{"class":100},[83,2437,2438],{"class":85,"line":181},[83,2439,261],{"class":100},[83,2441,2442],{"class":85,"line":194},[83,2443,114],{"emptyLinePlaceholder":113},[83,2445,2446],{"class":85,"line":207},[83,2447,2448],{"class":89},"# Async engine for FastAPI\n",[83,2450,2451,2454,2456],{"class":85,"line":213},[83,2452,2453],{"class":100},"async_engine ",[83,2455,123],{"class":96},[83,2457,126],{"class":100},[83,2459,2460,2462],{"class":85,"line":219},[83,2461,133],{"class":132},[83,2463,136],{"class":100},[83,2465,2466,2468,2470,2472],{"class":85,"line":232},[83,2467,222],{"class":142},[83,2469,123],{"class":96},[83,2471,240],{"class":166},[83,2473,136],{"class":100},[83,2475,2476,2478,2480,2483],{"class":85,"line":245},[83,2477,235],{"class":142},[83,2479,123],{"class":96},[83,2481,2482],{"class":166},"40",[83,2484,136],{"class":100},[83,2486,2487,2489,2491,2493,2496,2498,2500,2503],{"class":85,"line":258},[83,2488,143],{"class":142},[83,2490,123],{"class":96},[83,2492,799],{"class":100},[83,2494,2495],{"class":132},"\"statement_cache_size\"",[83,2497,163],{"class":100},[83,2499,167],{"class":166},[83,2501,2502],{"class":100},"},  ",[83,2504,2505],{"class":89},"# if behind PgBouncer\n",[83,2507,2509],{"class":85,"line":2508},18,[83,2510,261],{"class":100},[14,2512,264,2513,2515,2516,2520,2521,22],{},[27,2514,68],{}," for ",[18,2517,2519],{"href":2518},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","configuring async engines and connection pools"," covers pool tuning in depth. For cloud-hosted databases (RDS, CloudSQL, AlloyDB), see the ",[18,2522,2524],{"href":2523},"\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002F","connection pool tuning guide for cloud databases",[277,2526,2528],{"id":2527},"migrating-from-asyncpg-to-psycopg3","Migrating from asyncpg to psycopg3",[14,2530,2531,2532,2534],{},"psycopg3 offers better standards compliance and simpler ",[27,2533,530],{}," integration. The migration path is:",[2536,2537,2538,2549,2570,2576],"ol",{},[1921,2539,2540,2541,2544,2545,2548],{},"Replace ",[27,2542,2543],{},"postgresql+asyncpg:\u002F\u002F"," with ",[27,2546,2547],{},"postgresql+psycopg:\u002F\u002F"," in your DSN.",[1921,2550,2540,2551,2554,2555,2557,2558,2160,2560,2562,2563,2160,2566,2569],{},[27,2552,2553],{},"asyncpg","-specific ",[27,2556,68],{}," keys (",[27,2559,399],{},[27,2561,1744],{},") with psycopg3 equivalents (",[27,2564,2565],{},"binary",[27,2567,2568],{},"options"," for GUC parameters).",[1921,2571,2572,2573,2575],{},"Remove any custom asyncpg type codec registrations — psycopg3 uses ",[27,2574,1142],{}," text protocol which handles most types natively.",[1921,2577,2578,2579,2582],{},"Test RETURNING behaviour — psycopg3 uses ",[27,2580,2581],{},"cursor_factory"," parameter for some advanced result mapping.",[74,2584,2586],{"className":76,"code":2585,"language":78,"meta":79,"style":79},"# Before — asyncpg\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    connect_args={\n        \"statement_cache_size\": 0,\n        \"server_settings\": {\"application_name\": \"api\"},\n    },\n)\n\n# After — psycopg3\nengine = create_async_engine(\n    \"postgresql+psycopg:\u002F\u002Fuser:pass@localhost\u002Fmydb\",\n    connect_args={\n        \"options\": \"-c application_name=api\",  # GUC via options string\n    },\n)\n",[27,2587,2588,2593,2601,2607,2615,2625,2642,2646,2650,2654,2659,2667,2674,2682,2697,2701],{"__ignoreMap":79},[83,2589,2590],{"class":85,"line":86},[83,2591,2592],{"class":89},"# Before — asyncpg\n",[83,2594,2595,2597,2599],{"class":85,"line":93},[83,2596,120],{"class":100},[83,2598,123],{"class":96},[83,2600,126],{"class":100},[83,2602,2603,2605],{"class":85,"line":110},[83,2604,133],{"class":132},[83,2606,136],{"class":100},[83,2608,2609,2611,2613],{"class":85,"line":117},[83,2610,143],{"class":142},[83,2612,123],{"class":96},[83,2614,148],{"class":100},[83,2616,2617,2619,2621,2623],{"class":85,"line":129},[83,2618,160],{"class":132},[83,2620,163],{"class":100},[83,2622,167],{"class":166},[83,2624,136],{"class":100},[83,2626,2627,2629,2632,2635,2637,2640],{"class":85,"line":139},[83,2628,175],{"class":132},[83,2630,2631],{"class":100},": {",[83,2633,2634],{"class":132},"\"application_name\"",[83,2636,163],{"class":100},[83,2638,2639],{"class":132},"\"api\"",[83,2641,809],{"class":100},[83,2643,2644],{"class":85,"line":151},[83,2645,216],{"class":100},[83,2647,2648],{"class":85,"line":157},[83,2649,261],{"class":100},[83,2651,2652],{"class":85,"line":172},[83,2653,114],{"emptyLinePlaceholder":113},[83,2655,2656],{"class":85,"line":181},[83,2657,2658],{"class":89},"# After — psycopg3\n",[83,2660,2661,2663,2665],{"class":85,"line":194},[83,2662,120],{"class":100},[83,2664,123],{"class":96},[83,2666,126],{"class":100},[83,2668,2669,2672],{"class":85,"line":207},[83,2670,2671],{"class":132},"    \"postgresql+psycopg:\u002F\u002Fuser:pass@localhost\u002Fmydb\"",[83,2673,136],{"class":100},[83,2675,2676,2678,2680],{"class":85,"line":213},[83,2677,143],{"class":142},[83,2679,123],{"class":96},[83,2681,148],{"class":100},[83,2683,2684,2687,2689,2692,2694],{"class":85,"line":219},[83,2685,2686],{"class":132},"        \"options\"",[83,2688,163],{"class":100},[83,2690,2691],{"class":132},"\"-c application_name=api\"",[83,2693,382],{"class":100},[83,2695,2696],{"class":89},"# GUC via options string\n",[83,2698,2699],{"class":85,"line":232},[83,2700,216],{"class":100},[83,2702,2703],{"class":85,"line":245},[83,2704,261],{"class":100},[14,2706,2707,2708,22],{},"For a detailed comparison of asyncpg vs psycopg3 async drivers, see ",[18,2709,2711],{"href":2710},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fchoosing-between-asyncpg-and-psycopg-async-drivers\u002F","choosing between asyncpg and psycopg async drivers",[36,2713,2715],{"id":2714},"production-pitfalls-anti-patterns","Production Pitfalls & Anti-Patterns",[1918,2717,2718,2741,2761,2780,2804,2835],{},[1921,2719,2720,2727,2728,2731,2732,1153,2734,2737,2738,2740],{},[62,2721,2722,2723,2726],{},"Forgetting ",[27,2724,2725],{},"statement_cache_size=0"," behind PgBouncer transaction pooling",": Results in ",[27,2729,2730],{},"InvalidSQLStatementNameError"," in production, often after the first prepared statement is evicted from the server. Fix: always set both ",[27,2733,2725],{},[27,2735,2736],{},"prepared_statement_cache_size=0"," in ",[27,2739,68],{}," when using PgBouncer transaction mode.",[1921,2742,2743,2749,2750,2752,2753,2756,2757,2760],{},[62,2744,2745,2746,2748],{},"Using ",[27,2747,1172],{}," for JSON columns with asyncpg",": Raw text queries bypass SQLAlchemy's type codec registration. asyncpg returns JSON\u002FJSONB columns as ",[27,2751,1160],{},". Fix: use mapped ORM columns with ",[27,2754,2755],{},"Column(JSON)"," or call ",[27,2758,2759],{},"json.loads()"," explicitly on raw query results.",[1921,2762,2763,163,2766,2769,2770,2772,2773,69,2776,2779],{},[62,2764,2765],{},"aiosqlite with concurrent writes in tests",[27,2767,2768],{},"asyncio.gather()"," over multiple write coroutines using separate sessions raises ",[27,2771,548],{},". Fix: use ",[27,2774,2775],{},"StaticPool",[27,2777,2778],{},"NullPool"," with aiosqlite in tests, or use a single shared session per test.",[1921,2781,2782,2785,2786,2789,2790,2793,2794,2797,2798,2756,2801,2803],{},[62,2783,2784],{},"aiomysql implicit transaction left open",": Forgetting ",[27,2787,2788],{},"await session.commit()"," in a plain ",[27,2791,2792],{},"async with AsyncSession()"," block (without ",[27,2795,2796],{},".begin()",") leaves an open transaction on the server. MySQL may hold row locks until the connection times out. Fix: always use ",[27,2799,2800],{},"async with session.begin()",[27,2802,2788],{}," explicitly.",[1921,2805,2806,2815,2816,2818,2819,2821,2822,2824,2825,2160,2827,2829,2830,2832,2833,22],{},[62,2807,2808,2809,1153,2811,2814],{},"Mixing ",[27,2810,2293],{},[27,2812,2813],{},"$N"," parameter styles",": Dropping to ",[27,2817,2171],{}," and using SQLAlchemy-style ",[27,2820,2293],{}," parameters fails because asyncpg requires ",[27,2823,2159],{},"-style positional params. Fix: use ",[27,2826,2159],{},[27,2828,2163],{},", ... in raw asyncpg calls, and ",[27,2831,2293],{}," style only through SQLAlchemy's ",[27,2834,1172],{},[1921,2836,2837,163,2840,2842,2843,2845,2846,2849,2850,2075,2852,2075,2854,2857],{},[62,2838,2839],{},"RETURNING on MySQL or old SQLite",[27,2841,1912],{}," raises ",[27,2844,1949],{}," on MySQL and ",[27,2847,2848],{},"OperationalError"," on SQLite \u003C 3.35. Fix: use ",[27,2851,2074],{},[27,2853,2078],{},[27,2855,2856],{},"await session.refresh()"," for portable cross-dialect insert-and-fetch patterns.",[36,2859,2861],{"id":2860},"driver-quirk-comparison-matrix","Driver Quirk Comparison Matrix",[2863,2864,2867],"figure",{"className":2865},[2866],"diagram",[2868,2869,2874,2875,2874,2879,2874,2874,2883,2874,2874,2890,2874,2895,2874,2898,2905,2909,2913,2917,2874,2874,2921,2874,2926,2874,2929,2874,2932,2874,2935,2874,2874,2938,2874,2941,2874,2944,2874,2947,2874,2874,2950,2874,2954,2874,2956,2874,2958,2874,2961,2967,2971,2976,2979,2982,2985,2989,2874,2992,2996,3000,3004,3007,3009,2874,3012,3016,3020,3023,3026,3029,3032,3034,3036,2874,3041,3045,3049,3054,3056,3060,3063,2874,3066,3070,3074,3078,3080,3083,3086,2874,3089,3093,3097,3100,3103,3106,3109,3113],"svg",{"viewBox":2870,"role":2871,"ariaLabel":2872,"xmlns":2873},"0 0 760 400","img","Driver quirk comparison matrix across asyncpg, psycopg3, aiomysql, and aiosqlite","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[2876,2877,2878],"title",{},"Async Driver Quirk Comparison Matrix",[2880,2881,2882],"desc",{},"A grid comparing four async SQLAlchemy drivers across six behavioural dimensions: prepared statement caching, parameter style, JSON handling, RETURNING support, isolation default, and PgBouncer compatibility.",[2884,2885],"rect",{"x":167,"y":167,"width":2886,"height":2887,"fill":2888,"rx":2889},"760","400","#f1f9f6","8",[2884,2891],{"x":167,"y":167,"width":2892,"height":2893,"fill":2894,"rx":2889},"720","44","#0f766e",[2884,2896],{"x":167,"y":2897,"width":2892,"height":2889,"fill":2894},"36",[304,2899,2904],{"x":2900,"y":2901,"fill":2902,"style":2903},"110","27","#ffffff","text-anchor:middle;font-size:13px;font-weight:bold","\nFeature\n",[304,2906,2908],{"x":2907,"y":2901,"fill":2902,"style":2903},"270","\nasyncpg\n",[304,2910,2912],{"x":2911,"y":2901,"fill":2902,"style":2903},"390","\npsycopg3\n",[304,2914,2916],{"x":2915,"y":2901,"fill":2902,"style":2903},"510","\naiomysql\n",[304,2918,2920],{"x":2919,"y":2901,"fill":2902,"style":2903},"630","\naiosqlite\n",[85,2922],{"x1":167,"y1":2923,"x2":2892,"y2":2923,"stroke":2924,"style":2925},"100","rgba(15,118,110,0.28)","stroke-width:1",[85,2927],{"x1":167,"y1":2928,"x2":2892,"y2":2928,"stroke":2924,"style":2925},"156",[85,2930],{"x1":167,"y1":2931,"x2":2892,"y2":2931,"stroke":2924,"style":2925},"212",[85,2933],{"x1":167,"y1":2934,"x2":2892,"y2":2934,"stroke":2924,"style":2925},"268",[85,2936],{"x1":167,"y1":2937,"x2":2892,"y2":2937,"stroke":2924,"style":2925},"324",[85,2939],{"x1":2940,"y1":2893,"x2":2940,"y2":2887,"stroke":2924,"style":2925},"190",[85,2942],{"x1":2943,"y1":2893,"x2":2943,"y2":2887,"stroke":2924,"style":2925},"340",[85,2945],{"x1":2946,"y1":2893,"x2":2946,"y2":2887,"stroke":2924,"style":2925},"460",[85,2948],{"x1":2949,"y1":2893,"x2":2949,"y2":2887,"stroke":2924,"style":2925},"580",[2884,2951],{"x":167,"y":2893,"width":2892,"height":2952,"fill":2953,"rx":167},"56","#e8f4f1",[2884,2955],{"x":167,"y":2928,"width":2892,"height":2952,"fill":2953,"rx":167},[2884,2957],{"x":167,"y":2934,"width":2892,"height":2952,"fill":2953,"rx":167},[2884,2959],{"x":167,"y":2960,"width":2892,"height":240,"fill":2953,"rx":167},"380",[304,2962,2966],{"x":2900,"y":2963,"fill":2964,"style":2965},"67","#113f39","text-anchor:middle;font-size:12px;font-weight:bold","\nPrepared\n",[304,2968,2970],{"x":2900,"y":2969,"fill":2964,"style":2965},"83","\nstatement cache\n",[304,2972,2975],{"x":2907,"y":2963,"fill":2973,"style":2974},"#3f4f4b","text-anchor:middle;font-size:12px","\nLRU, on by default\n",[304,2977,2978],{"x":2907,"y":2969,"fill":2973,"style":2974},"\ncache_size=0 to off\n",[304,2980,2981],{"x":2911,"y":2963,"fill":2973,"style":2974},"\nCursor-level,\n",[304,2983,2984],{"x":2911,"y":2969,"fill":2973,"style":2974},"\nbinary protocol\n",[304,2986,2988],{"x":2915,"y":2987,"fill":2973,"style":2974},"75","\nN\u002FA (text proto)\n",[304,2990,2991],{"x":2919,"y":2987,"fill":2973,"style":2974},"\nN\u002FA (SQLite)\n",[304,2993,2995],{"x":2900,"y":2994,"fill":2964,"style":2965},"123","\nParameter\n",[304,2997,2999],{"x":2900,"y":2998,"fill":2964,"style":2965},"139","\nstyle\n",[304,3001,3003],{"x":2907,"y":3002,"fill":2973,"style":2974},"131","\n$1, $2 (positional)\n",[304,3005,3006],{"x":2911,"y":3002,"fill":2973,"style":2974},"\n%s (pyformat)\n",[304,3008,3006],{"x":2915,"y":3002,"fill":2973,"style":2974},[304,3010,3011],{"x":2919,"y":3002,"fill":2973,"style":2974},"\n? (qmark)\n",[304,3013,3015],{"x":2900,"y":3014,"fill":2964,"style":2965},"179","\nJSON\u002FJSONB\n",[304,3017,3019],{"x":2900,"y":3018,"fill":2964,"style":2965},"195","\nraw text()\n",[304,3021,3022],{"x":2907,"y":3014,"fill":2973,"style":2974},"\nReturns str —\n",[304,3024,3025],{"x":2907,"y":3018,"fill":2973,"style":2974},"\nmust json.loads()\n",[304,3027,3028],{"x":2911,"y":3014,"fill":2973,"style":2974},"\nReturns dict\n",[304,3030,3031],{"x":2911,"y":3018,"fill":2973,"style":2974},"\nnatively\n",[304,3033,3022],{"x":2915,"y":3014,"fill":2973,"style":2974},[304,3035,3025],{"x":2915,"y":3018,"fill":2973,"style":2974},[304,3037,3040],{"x":2919,"y":3038,"fill":2973,"style":3039},"187","text-anchor:middle;font-size:11px","\nNo native JSON\n",[304,3042,3044],{"x":2900,"y":3043,"fill":2964,"style":2965},"235","\nRETURNING\n",[304,3046,3048],{"x":2900,"y":3047,"fill":2964,"style":2965},"251","\nclause\n",[304,3050,3053],{"x":2907,"y":3051,"fill":3052,"style":2965},"243","#1f9f95","\nFull support\n",[304,3055,3053],{"x":2911,"y":3051,"fill":3052,"style":2965},[304,3057,3059],{"x":2915,"y":3051,"fill":3058,"style":2965},"#c0392b","\nNot supported\n",[304,3061,3062],{"x":2919,"y":3043,"fill":2973,"style":2974},"\nSQLite ≥ 3.35\n",[304,3064,3065],{"x":2919,"y":3047,"fill":2973,"style":2974},"\nonly\n",[304,3067,3069],{"x":2900,"y":3068,"fill":2964,"style":2965},"291","\nIsolation level\n",[304,3071,3073],{"x":2900,"y":3072,"fill":2964,"style":2965},"307","\ndefault\n",[304,3075,3077],{"x":2907,"y":3076,"fill":2973,"style":2974},"299","\nREAD COMMITTED\n",[304,3079,3077],{"x":2911,"y":3076,"fill":2973,"style":2974},[304,3081,3082],{"x":2915,"y":3068,"fill":2973,"style":3039},"\nREPEATABLE READ\n",[304,3084,3085],{"x":2915,"y":3072,"fill":2973,"style":3039},"\n(MySQL default)\n",[304,3087,3088],{"x":2919,"y":3076,"fill":2973,"style":2974},"\nSERIALIZABLE\n",[304,3090,3092],{"x":2900,"y":3091,"fill":2964,"style":2965},"347","\nPgBouncer txn\n",[304,3094,3096],{"x":2900,"y":3095,"fill":2964,"style":2965},"363","\nmode compat.\n",[304,3098,3099],{"x":2907,"y":3091,"fill":3058,"style":2965},"\nBreaks by default\n",[304,3101,3102],{"x":2907,"y":3095,"fill":2973,"style":2974},"\ncache_size=0 fixes\n",[304,3104,3105],{"x":2911,"y":3091,"fill":2973,"style":2974},"\nWorks with\n",[304,3107,3108],{"x":2911,"y":3095,"fill":2973,"style":2974},"\nbinary=False\n",[304,3110,3112],{"x":2915,"y":3111,"fill":2973,"style":2974},"355","\nN\u002FA (MySQL)\n",[304,3114,2991],{"x":2919,"y":3111,"fill":2973,"style":2974},[36,3116,3118],{"id":3117},"frequently-asked-questions","Frequently Asked Questions",[14,3120,3121,3127,3128,3130],{},[62,3122,3123,3124,3126],{},"Why do I see ",[27,3125,2730],{}," only after several minutes of traffic, not immediately?","\nasyncpg fills its prepared statement LRU cache gradually. The error surfaces when a cached statement handle references a backend server that has been swapped out by PgBouncer's transaction pooling, which only happens once the pool has rotated connections under load. The fix — setting ",[27,3129,2725],{}," — must be applied before any connections are created; restarting the engine after the fact requires replacing the engine object entirely.",[14,3132,3133,3136,3137,3139,3140,3143],{},[62,3134,3135],{},"Does psycopg3 have the same PgBouncer problem as asyncpg?","\nNot with the default text protocol. psycopg3 only sends ",[27,3138,286],{}," messages in binary protocol mode, which can be disabled with ",[27,3141,3142],{},"binary=False",". In the default text protocol mode, psycopg3 is fully compatible with PgBouncer transaction pooling without any extra configuration.",[14,3145,3146,3152,3153,3155,3156,3159,3160,3163],{},[62,3147,3148,3149,3151],{},"Can I use asyncpg's ",[27,3150,530],{}," through SQLAlchemy's async session?","\nNo. ",[27,3154,530],{}," requires a dedicated long-lived connection that is not returned to the pool between events. You must obtain a raw asyncpg connection via ",[27,3157,3158],{},"await engine.raw_connection()",", call ",[27,3161,3162],{},"await conn.add_listener(channel, callback)",", and hold that connection outside the pool for the lifetime of the listener.",[14,3165,3166,3173,3174,3176,3177,3179,3180,3182,3183,3186,3187,3189],{},[62,3167,3168,3169,3172],{},"My aiosqlite tests fail with ",[27,3170,3171],{},"database is locked"," when run in parallel. What's the fix?","\nUse ",[27,3175,2775],{}," (single shared connection) or ",[27,3178,2778],{}," (a new connection per operation) with aiosqlite in test fixtures. ",[27,3181,2775],{}," is preferable for in-memory databases (",[27,3184,3185],{},"sqlite+aiosqlite:\u002F\u002F\u002F:memory:",") because it guarantees all operations share the same SQLite database instance. Never use ",[27,3188,2768],{}," across sessions that each open their own aiosqlite connection to the same file in write mode.",[14,3191,3192,3198,3200,3201,3204],{},[62,3193,3194,3195,3197],{},"Why does ",[27,3196,1912],{}," work in development (SQLite 3.38) but fail in CI (SQLite 3.31)?",[27,3199,1908],{}," was added in SQLite 3.35. SQLite ships with the OS on many CI images; Ubuntu 20.04 ships 3.31, Ubuntu 22.04 ships 3.37. Fix: pin the SQLite version in CI by installing ",[27,3202,3203],{},"pysqlite3-binary",", which bundles a recent SQLite build, or switch CI to a newer OS image.",[36,3206,3208],{"id":3207},"related","Related",[1918,3210,3211,3222,3228,3234,3254],{},[1921,3212,3213,3216,3217,1153,3219,3221],{},[18,3214,3215],{"href":422},"Handling asyncpg Prepared Statement Errors with PgBouncer"," — step-by-step fix for ",[27,3218,2730],{},[27,3220,414],{}," behind PgBouncer transaction pooling.",[1921,3223,3224,3227],{},[18,3225,3226],{"href":2710},"Choosing Between asyncpg and psycopg Async Drivers"," — performance benchmarks, feature matrix, and migration guidance between the two PostgreSQL async drivers.",[1921,3229,3230,3233],{},[18,3231,3232],{"href":837},"Selecting Async Drivers for SQLite, MySQL, and Postgres"," — when to use aiosqlite vs aiomysql vs asyncpg, with install commands and engine DSN examples.",[1921,3235,3236,3239,3240,2160,3243,2160,3246,3249,3250,3253],{},[18,3237,3238],{"href":2523},"Tuning Connection Pools for Cloud Databases"," — ",[27,3241,3242],{},"pool_size",[27,3244,3245],{},"max_overflow",[27,3247,3248],{},"pool_recycle",", and ",[27,3251,3252],{},"pool_pre_ping"," settings for RDS, CloudSQL, and PgBouncer-fronted clusters.",[1921,3255,3256,3259],{},[18,3257,3258],{"href":20},"Async Engines, Dialects, and Connection Pooling"," — parent guide covering the full async engine stack from driver selection to pool configuration.",[3261,3262,3263],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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}",{"title":79,"searchDepth":93,"depth":93,"links":3265},[3266,3267,3271,3275,3281,3285,3286,3287,3288],{"id":38,"depth":93,"text":39},{"id":274,"depth":93,"text":275,"children":3268},[3269,3270],{"id":279,"depth":110,"text":280},{"id":426,"depth":110,"text":427},{"id":534,"depth":93,"text":535,"children":3272},[3273,3274],{"id":538,"depth":110,"text":539},{"id":842,"depth":110,"text":843},{"id":1131,"depth":93,"text":1132,"children":3276},[3277,3278,3279,3280],{"id":1135,"depth":110,"text":1136},{"id":1733,"depth":110,"text":1734},{"id":1902,"depth":110,"text":1903},{"id":2152,"depth":110,"text":2153},{"id":2349,"depth":93,"text":2350,"children":3282},[3283,3284],{"id":2353,"depth":110,"text":2354},{"id":2527,"depth":110,"text":2528},{"id":2714,"depth":93,"text":2715},{"id":2860,"depth":93,"text":2861},{"id":3117,"depth":93,"text":3118},{"id":3207,"depth":93,"text":3208},"Async SQLAlchemy is not one thing — it is a thin async shim over four distinct database drivers, each with its own connection model, type encoding pipeline, and server negotiation protocol, all covered in the async engines, dialects, and connection pooling guide.","md",{"date":3292},"2026-06-18","\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks",{"title":5,"description":3289},"async-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Findex","a284lBwgwCzv8Q1eu66hy5BWRuTF5EihLhxIkZ-bp98",1781810028983]