Case study 09 · SQL · web reporting

Messy CSVsto six reports

A small reporting app built the way a lot of real business reporting starts: messy exports, a cleaning loader, a normalised SQLite database, and raw SQL behind six reports you can filter, chart and export. No ORM, so every query is visible.

DeployedFlask · SQLite · Chart.js

Why raw SQLWindow functions, CTEs and query plans are the skills on show. An ORM would hide them.

6Reports
4Tables, normalised
3Indexes, checked with EXPLAIN
0String-built queries
Data flow · how a record moves

Messy CSVs to six reports

Crossed-out dots are duplicate customer emails removed by the loader. Orphan orders and missing amounts are kept on purpose, so the data-quality report can show them. Click a stage to read what happens there.

01 · Load

Clean on the way in

The loader standardises region names (trimming, title case, missing becomes Unknown), reads three date formats (YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY), and removes duplicate customer emails. It deliberately keeps orphan orders and null amounts, so the data-quality report can show them instead of the loader silently dropping them.

regionorder dateemailstep
 north 07/03/2024a.k@mail.comregion, date
South03/08/2024r.s@mail.comdate
WEST2024-03-09a.k@mail.comdedupe
East2024-03-11no customerkept · orphan

Illustration · sample rows, not the real data

Pipelinemessy CSVs → seed/generate.py → seed/load.py → reports.db → Flask.

02 · Model

Four tables, three joins

A customer has many orders, an order has many line items, and each line item points to one product. Indexes sit on the columns the reports filter and join on.

Indexesidx_orders_date, idx_orders_cust, idx_items_order. The app has an endpoint that shows the EXPLAIN QUERY PLAN for each report.

03 · Reports

Six questions, six queries

Pick a report to see what it answers and the SQL idea behind it.

ExportEvery report downloads as CSV or XLSX, with the same filters applied.

04 · Safe & tested

Boring in the right places

01ParamsEvery filter goes through :named parameter binding. User input never touches the SQL string.
02PlansEXPLAIN QUERY PLAN endpoint to confirm the indexes are actually used.
03TestsA pytest suite, run with pytest -q.
→NextPLANNEDPostgres, saved views and scheduled snapshots, listed in the README as next steps.
SQLSQLiteWindow functionsFlaskJinjaChart.jspytestVercel

Front endJinja templates with vanilla JS and Chart.js. Light on purpose.