Lesson Complete!
Powerful Queries
What you added
Three query capabilities that would have been painful with the JSON file:
- Date range filtering —
GET /expenses?start=2024-01-01&end=2024-03-31lets clients retrieve expenses within a specific period. Chained.where()conditions handle this at the database level — no Python loops, no loading all rows first. - SQL aggregation —
GET /summarynow usesfunc.sum()and.group_by(). The database computes the totals and returns one row per category. Previously, your app loaded every expense row to compute 5 numbers. - Description search —
GET /expenses?search=coffeefinds expenses whose description contains the search term. SQLModel's.contains()maps directly to a SQLLIKEquery.
These can be combined. GET /expenses?category=food&start=2024-01-01&search=lunch returns all food expenses from January onward that mention lunch in the description.
Where to go from here
SQLite is the right choice for personal projects, prototypes, and single-user APIs. When you need multiple users writing to the same database at the same time, the upgrade path is PostgreSQL.
The migration is straightforward: everything you wrote — SQLModel, Session, select(), .where(), .group_by(), func.sum() — works identically with PostgreSQL. You change the connection URL from sqlite:///expenses.db to a PostgreSQL connection string, and the rest stays the same. Hosted options include Supabase, Neon, and Railway — all provide managed PostgreSQL with no server to set up.
What comes next
Your tests from Course 2 are broken — they still use the old dict-based app with a global TestClient. Lesson 4 rewrites the test suite to run against an in-memory SQLite database, giving each test a fresh, isolated database.