ProvenPin Install

Piccolo ORM CRUD and migrations-style setup on sqlite: working versions (tested 2026-10-15)

Async Piccolo tables with a foreign key, a migrations-style schema built by create_db_tables, CRUD helpers, and offline tests on a sqlite file with AsyncTableTest and ModelBuilder.

passed6 passed · 0 failed2026-10-15 21:56 UTC
Status
passed
Last passed
2026-10-15 21:56 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions3
piccolo@1.36.0aiosqlite@0.22.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38813530570

The code

Exactly the files that passed. Your agent can pull them with get_recipe("piccolo-sqlite-crud") (install), or see them on GitHub.

requirements.txt
piccolo==1.36.0
aiosqlite==0.22.1
pytest==9.1.1
db.py
import tempfile
from pathlib import Path

from piccolo.columns import ForeignKey, Integer, Varchar
from piccolo.engine.sqlite import SQLiteEngine
from piccolo.table import Table

# A real file is required: with path=":memory:" every Piccolo query opens a
# new connection, so each one would see an empty database.
DB_PATH = Path(tempfile.mkdtemp(prefix="piccolo-")) / "app.sqlite"
DB = SQLiteEngine(path=str(DB_PATH))


class Band(Table, db=DB):
    name = Varchar(unique=True)
    popularity = Integer(default=0)


class Album(Table, db=DB):
    name = Varchar()
    band = ForeignKey(references=Band)
crud.py
from db import Album, Band
from piccolo.table import create_db_tables, drop_db_tables


async def create_schema() -> None:
    # create_db_tables emits the same DDL migrations would, and sorts
    # tables so foreign keys are created after the tables they reference.
    await create_db_tables(Band, Album, if_not_exists=True)


async def drop_schema() -> None:
    await drop_db_tables(Band, Album)


async def add_band(name: str, popularity: int = 0) -> Band:
    band = Band(name=name, popularity=popularity)
    await band.save()
    return band


async def top_bands(limit: int = 10) -> list[Band]:
    return (
        await Band.objects()
        .order_by(Band.popularity, ascending=False)
        .limit(limit)
    )


async def rename_band(band_id: int, name: str) -> dict | None:
    rows = (
        await Band.update({Band.name: name})
        .where(Band.id == band_id)
        .returning(Band.id, Band.name)
    )
    return rows[0] if rows else None


async def add_album(name: str, band: Band | int) -> Album:
    return await Album.objects().create(name=name, band=band)
tests/test_crud.py
import sqlite3

from crud import add_album, add_band, rename_band, top_bands
from db import Album, Band
from piccolo.testing import ModelBuilder
from piccolo.testing.test_case import AsyncTableTest


class TestPiccoloCrud(AsyncTableTest):
    tables = [Band, Album]

    async def test_select_returns_dicts_and_missing_rows_return_none(self):
        await Band.insert(Band(name="Python", popularity=100))
        self.assertEqual(await Band.select(Band.name), [{"name": "Python"}])
        self.assertIsNone(
            await Band.objects().where(Band.name == "Nope").first()
        )
        self.assertIsNone(await Band.objects().get(Band.name == "Nope"))

    async def test_objects_return_rows_and_apply_ordering(self):
        await add_band("Python", 100)
        await add_band("Django", 90)
        self.assertEqual(
            [band.name for band in await top_bands()], ["Python", "Django"]
        )
        band = await Band.objects().where(Band.name == "Python").first()
        self.assertEqual(band.name, "Python")

    async def test_update_and_delete_return_nothing_without_returning(self):
        await add_band("Python", 100)
        empty = (
            await Band.update({Band.popularity: 1})
            .where(Band.name == "Python")
        )
        self.assertEqual(empty, [])
        self.assertEqual(await rename_band(1, "Py"), {"id": 1, "name": "Py"})

    async def test_foreign_key_accepts_row_instance_not_selected_dict(self):
        band = await add_band("Python", 100)
        album = await add_album("First", band)
        self.assertEqual(album.band.id, band.id)
        row = await Band.select().first()
        with self.assertRaises(sqlite3.ProgrammingError):
            await add_album("Bad", row)

    async def test_get_or_create_flags_was_created(self):
        band = await Band.objects().get_or_create(
            Band.name == "Go", defaults={Band.popularity: 70}
        )
        self.assertTrue(band._was_created)
        again = await Band.objects().get_or_create(
            Band.name == "Go", defaults={Band.popularity: 999}
        )
        self.assertFalse(again._was_created)
        self.assertEqual(again.popularity, 70)

    async def test_model_builder_persists_and_creates_parents(self):
        album = await ModelBuilder.build(Album)
        self.assertIsNotNone(album.id)
        self.assertEqual(await Band.count().run(), 1)
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.