Skip to content

Commit 2d4cd6a

Browse files
committed
fixes #6
1 parent ef9e802 commit 2d4cd6a

3 files changed

Lines changed: 249 additions & 6 deletions

File tree

fastasyncpg/_modidx.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
'fastasyncpg.core.Database.__init__': ('core.html#database.__init__', 'fastasyncpg/core.py'),
1111
'fastasyncpg.core.Database.__str__': ('core.html#database.__str__', 'fastasyncpg/core.py'),
1212
'fastasyncpg.core.Database._retr_tbl': ('core.html#database._retr_tbl', 'fastasyncpg/core.py'),
13+
'fastasyncpg.core.Database.acquire': ('core.html#database.acquire', 'fastasyncpg/core.py'),
1314
'fastasyncpg.core.Database.create': ('core.html#database.create', 'fastasyncpg/core.py'),
1415
'fastasyncpg.core.Database.create_from_schema': ( 'core.html#database.create_from_schema',
1516
'fastasyncpg/core.py'),
17+
'fastasyncpg.core.Database.from_meta': ('core.html#database.from_meta', 'fastasyncpg/core.py'),
1618
'fastasyncpg.core.Database.get_tables': ('core.html#database.get_tables', 'fastasyncpg/core.py'),
1719
'fastasyncpg.core.Database.item': ('core.html#database.item', 'fastasyncpg/core.py'),
1820
'fastasyncpg.core.Database.link_dcs': ('core.html#database.link_dcs', 'fastasyncpg/core.py'),
@@ -22,6 +24,7 @@
2224
'fastasyncpg.core.Database.t': ('core.html#database.t', 'fastasyncpg/core.py'),
2325
'fastasyncpg.core.Database.table': ('core.html#database.table', 'fastasyncpg/core.py'),
2426
'fastasyncpg.core.Database.table2glb': ('core.html#database.table2glb', 'fastasyncpg/core.py'),
27+
'fastasyncpg.core.Database.transaction': ('core.html#database.transaction', 'fastasyncpg/core.py'),
2528
'fastasyncpg.core.FRecord': ('core.html#frecord', 'fastasyncpg/core.py'),
2629
'fastasyncpg.core.FRecord.__getattr__': ('core.html#frecord.__getattr__', 'fastasyncpg/core.py'),
2730
'fastasyncpg.core.FRecord._repr_html_': ('core.html#frecord._repr_html_', 'fastasyncpg/core.py'),

fastasyncpg/core.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
# %% ../nbs/00_core.ipynb #222d751e
99
from fastcore.utils import *
10+
from contextlib import asynccontextmanager
1011
import asyncpg
1112
from asyncpg import connection,protocol
1213

@@ -586,3 +587,30 @@ async def create_pool(*args, **kwargs):
586587
res = Database(pool, refresh=False)
587588
await res.refresh()
588589
return res
590+
591+
# %% ../nbs/00_core.ipynb #7072a71f
592+
@patch(cls_method=True)
593+
def from_meta(cls:Database, conn, db):
594+
"Create a Database sharing metadata from `db` but using `conn`"
595+
res = cls(conn, refresh=False)
596+
res._tnames,res._vnames,res._cols,res._pks = db._tnames,db._vnames,db._cols,db._pks
597+
for name,tbl in db._tables.items():
598+
if hasattr(tbl, 'cls'): res.table(name).cls = tbl.cls
599+
return res
600+
601+
# %% ../nbs/00_core.ipynb #3e4e3b18
602+
@patch
603+
@asynccontextmanager
604+
async def acquire(self:Database):
605+
"Context manager yielding a Database on a single pool connection"
606+
async with self.conn.acquire() as conn:
607+
yield Database.from_meta(conn, self)
608+
609+
# %% ../nbs/00_core.ipynb #f0b7826a
610+
@patch
611+
@asynccontextmanager
612+
async def transaction(self:Database):
613+
"Context manager yielding a transactional Database on a single connection"
614+
async with self.acquire() as db:
615+
async with db.conn.transaction():
616+
yield db

nbs/00_core.ipynb

Lines changed: 218 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"name": "stdout",
8181
"output_type": "stream",
8282
"text": [
83-
"psql (PostgreSQL) 18.1 (Homebrew)\r\n"
83+
"/opt/homebrew/bin/bash: line 1: psql: command not found\r\n"
8484
]
8585
}
8686
],
@@ -140,11 +140,7 @@
140140
"name": "stdout",
141141
"output_type": "stream",
142142
"text": [
143-
" version \r\n",
144-
"-----------------------------------------------------------------------------------------------------------------------------\r\n",
145-
" PostgreSQL 18.1 (Homebrew) on aarch64-apple-darwin25.2.0, compiled by Apple clang version 17.0.0 (clang-1700.6.3.2), 64-bit\r\n",
146-
"(1 row)\r\n",
147-
"\r\n"
143+
"/opt/homebrew/bin/bash: line 1: psql: command not found\r\n"
148144
]
149145
}
150146
],
@@ -221,6 +217,7 @@
221217
"source": [
222218
"#| export\n",
223219
"from fastcore.utils import *\n",
220+
"from contextlib import asynccontextmanager\n",
224221
"import asyncpg\n",
225222
"from asyncpg import connection,protocol"
226223
]
@@ -4328,6 +4325,221 @@
43284325
"source": [
43294326
"str(db)"
43304327
]
4328+
},
4329+
{
4330+
"cell_type": "markdown",
4331+
"id": "1437fd39",
4332+
"metadata": {},
4333+
"source": [
4334+
"## Context managers"
4335+
]
4336+
},
4337+
{
4338+
"cell_type": "code",
4339+
"execution_count": null,
4340+
"id": "7072a71f",
4341+
"metadata": {},
4342+
"outputs": [],
4343+
"source": [
4344+
"#| export\n",
4345+
"@patch(cls_method=True)\n",
4346+
"def from_meta(cls:Database, conn, db):\n",
4347+
" \"Create a Database sharing metadata from `db` but using `conn`\"\n",
4348+
" res = cls(conn, refresh=False)\n",
4349+
" res._tnames,res._vnames,res._cols,res._pks = db._tnames,db._vnames,db._cols,db._pks\n",
4350+
" for name,tbl in db._tables.items():\n",
4351+
" if hasattr(tbl, 'cls'): res.table(name).cls = tbl.cls\n",
4352+
" return res"
4353+
]
4354+
},
4355+
{
4356+
"cell_type": "markdown",
4357+
"id": "aa0a9cd6",
4358+
"metadata": {},
4359+
"source": [
4360+
"`from_meta` creates a new `Database` that shares cached metadata (table names, column info, primary keys, and dataclass registrations) from an existing database, but routes queries through a different connection. This avoids re-querying the schema when you need a separate connection handle."
4361+
]
4362+
},
4363+
{
4364+
"cell_type": "code",
4365+
"execution_count": null,
4366+
"id": "09157f97",
4367+
"metadata": {},
4368+
"outputs": [
4369+
{
4370+
"name": "stdout",
4371+
"output_type": "stream",
4372+
"text": [
4373+
"postgresql://jhoward@127.0.0.1:5432/chinook\n",
4374+
"[<FRecord artist_id=1 name='AC/DC'>, <FRecord artist_id=2 name='Accept'>]\n"
4375+
]
4376+
}
4377+
],
4378+
"source": [
4379+
"async with db.conn.acquire() as conn:\n",
4380+
" db2 = Database.from_meta(conn, db)\n",
4381+
" print(str(db2))\n",
4382+
" print(await db2.t.artist(limit=2))"
4383+
]
4384+
},
4385+
{
4386+
"cell_type": "code",
4387+
"execution_count": null,
4388+
"id": "3e4e3b18",
4389+
"metadata": {},
4390+
"outputs": [],
4391+
"source": [
4392+
"#| export\n",
4393+
"@patch\n",
4394+
"@asynccontextmanager\n",
4395+
"async def acquire(self:Database):\n",
4396+
" \"Context manager yielding a Database on a single pool connection\"\n",
4397+
" async with self.conn.acquire() as conn:\n",
4398+
" yield Database.from_meta(conn, self)"
4399+
]
4400+
},
4401+
{
4402+
"cell_type": "markdown",
4403+
"id": "d2664aac",
4404+
"metadata": {},
4405+
"source": [
4406+
"`acquire` is a convenience context manager for pool-backed databases. It acquires a single connection from the pool and yields a `Database` wrapping it (with shared metadata via `from_meta`). This is useful when you need multiple queries to run on the same connection — for example, with cursors or advisory locks."
4407+
]
4408+
},
4409+
{
4410+
"cell_type": "code",
4411+
"execution_count": null,
4412+
"id": "258b0516",
4413+
"metadata": {},
4414+
"outputs": [
4415+
{
4416+
"name": "stdout",
4417+
"output_type": "stream",
4418+
"text": [
4419+
"postgresql://jhoward@127.0.0.1:5432/chinook\n",
4420+
"[<FRecord artist_id=1 name='AC/DC'>, <FRecord artist_id=2 name='Accept'>]\n"
4421+
]
4422+
}
4423+
],
4424+
"source": [
4425+
"async with db.acquire() as sdb:\n",
4426+
" print(str(sdb))\n",
4427+
" print(await sdb.t.artist(limit=2))"
4428+
]
4429+
},
4430+
{
4431+
"cell_type": "code",
4432+
"execution_count": null,
4433+
"id": "f0b7826a",
4434+
"metadata": {},
4435+
"outputs": [],
4436+
"source": [
4437+
"#| export\n",
4438+
"@patch\n",
4439+
"@asynccontextmanager\n",
4440+
"async def transaction(self:Database):\n",
4441+
" \"Context manager yielding a transactional Database on a single connection\"\n",
4442+
" async with self.acquire() as db:\n",
4443+
" async with db.conn.transaction():\n",
4444+
" yield db"
4445+
]
4446+
},
4447+
{
4448+
"cell_type": "markdown",
4449+
"id": "3bb0a9fb",
4450+
"metadata": {},
4451+
"source": [
4452+
"`transaction` builds on `acquire` — it acquires a connection, starts a PostgreSQL transaction on it, and yields the transactional `Database`. All operations on the yielded object are atomic: they either all commit when the block exits normally, or all roll back if an exception is raised.\n",
4453+
"\n",
4454+
"Inside a transaction, all operations are atomic. Here we verify that a failed operation rolls back the insert:"
4455+
]
4456+
},
4457+
{
4458+
"cell_type": "code",
4459+
"execution_count": null,
4460+
"id": "83a365ae",
4461+
"metadata": {},
4462+
"outputs": [
4463+
{
4464+
"name": "stdout",
4465+
"output_type": "stream",
4466+
"text": [
4467+
"Before: 275, After: 275 — rollback worked!\n"
4468+
]
4469+
}
4470+
],
4471+
"source": [
4472+
"count_before = await db.t.artist.count\n",
4473+
"next_id = (await db.q(\"SELECT MAX(artist_id) as m FROM artist\"))[0]['m'] + 1\n",
4474+
"\n",
4475+
"try:\n",
4476+
" async with db.transaction() as txn:\n",
4477+
" await txn.t.artist.insert(artist_id=next_id, name='Test Artist')\n",
4478+
" raise ValueError(\"something went wrong\")\n",
4479+
"except ValueError: pass\n",
4480+
"\n",
4481+
"count_after = await db.t.artist.count\n",
4482+
"assert count_before == count_after\n",
4483+
"print(f\"Before: {count_before}, After: {count_after} — rollback worked!\")"
4484+
]
4485+
},
4486+
{
4487+
"cell_type": "markdown",
4488+
"id": "ff1dc1bb",
4489+
"metadata": {},
4490+
"source": [
4491+
"And when the transaction succeeds, the changes are committed:"
4492+
]
4493+
},
4494+
{
4495+
"cell_type": "code",
4496+
"execution_count": null,
4497+
"id": "21742d66",
4498+
"metadata": {},
4499+
"outputs": [
4500+
{
4501+
"name": "stdout",
4502+
"output_type": "stream",
4503+
"text": [
4504+
"Before: 275, After: 276 — commit worked!\n"
4505+
]
4506+
},
4507+
{
4508+
"data": {
4509+
"text/html": [
4510+
"<table class=\"prose\"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody><tr><td>artist_id</td><td>276</td></tr><tr><td>name</td><td>Committed Artist</td></tr></tbody></table>"
4511+
],
4512+
"text/plain": [
4513+
"<FRecord artist_id=276 name='Committed Artist'>"
4514+
]
4515+
},
4516+
"execution_count": null,
4517+
"metadata": {},
4518+
"output_type": "execute_result"
4519+
}
4520+
],
4521+
"source": [
4522+
"count_before = await db.t.artist.count\n",
4523+
"\n",
4524+
"async with db.transaction() as txn:\n",
4525+
" await txn.t.artist.insert(artist_id=next_id, name='Committed Artist')\n",
4526+
"\n",
4527+
"count_after = await db.t.artist.count\n",
4528+
"assert count_after == count_before + 1\n",
4529+
"print(f\"Before: {count_before}, After: {count_after} — commit worked!\")\n",
4530+
"\n",
4531+
"await db.t.artist.delete(next_id)"
4532+
]
4533+
},
4534+
{
4535+
"cell_type": "code",
4536+
"execution_count": null,
4537+
"id": "c7e5cb98",
4538+
"metadata": {},
4539+
"outputs": [],
4540+
"source": [
4541+
"await db.close()"
4542+
]
43314543
}
43324544
],
43334545
"metadata": {},

0 commit comments

Comments
 (0)