|
80 | 80 | "name": "stdout", |
81 | 81 | "output_type": "stream", |
82 | 82 | "text": [ |
83 | | - "psql (PostgreSQL) 18.1 (Homebrew)\r\n" |
| 83 | + "/opt/homebrew/bin/bash: line 1: psql: command not found\r\n" |
84 | 84 | ] |
85 | 85 | } |
86 | 86 | ], |
|
140 | 140 | "name": "stdout", |
141 | 141 | "output_type": "stream", |
142 | 142 | "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" |
148 | 144 | ] |
149 | 145 | } |
150 | 146 | ], |
|
221 | 217 | "source": [ |
222 | 218 | "#| export\n", |
223 | 219 | "from fastcore.utils import *\n", |
| 220 | + "from contextlib import asynccontextmanager\n", |
224 | 221 | "import asyncpg\n", |
225 | 222 | "from asyncpg import connection,protocol" |
226 | 223 | ] |
|
4328 | 4325 | "source": [ |
4329 | 4326 | "str(db)" |
4330 | 4327 | ] |
| 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 | + ] |
4331 | 4543 | } |
4332 | 4544 | ], |
4333 | 4545 | "metadata": {}, |
|
0 commit comments