Lesson Complete!

Powerful Queries

What you added

Three query capabilities that would have been painful with the JSON file:

  • Date range filteringGET /expenses?start=2024-01-01&end=2024-03-31 lets 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 aggregationGET /summary now uses func.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 searchGET /expenses?search=coffee finds expenses whose description contains the search term. SQLModel's .contains() maps directly to a SQL LIKE query.

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.