Store and retain MQTT time-series data in SQLite
Ingest a small MQTT stream into SQLite and preserve schema, query, late-data, retention, and restore evidence without Docker or a cloud account.

Data Dora: I want you to prove what arrived, what was stored, what a rollup hides, and what must be restored before deletion.
Predict the reading, then compare it with the measurement.
Python 3 in your browser (JupyterLite)
Python · no installIngest a small MQTT stream into SQLite and preserve schema, query, late-data, retention, and restore evidence without Docker or a cloud account.
Open the notebook in your browser and run each Python cell; no install or account is needed.
Three ways to run: use JupyterLite here with no install; run main.py locally from the downloadable lab folder; or open the same notebook in Google Colab.
Steps
Step 1
- Do
- Run `python3 main.py --step 1` to create the in-memory readings table and inspect its index contract.
- You will see
- The terminal prints the `CREATE TABLE` and `CREATE INDEX` SQL, then the PRAGMA rows `0 sensor` and `1 observed_at`.
- Why it matters
- Seeing both schema statements and SQLite's returned index metadata makes identity and observed-time access reviewable before data arrives.

Step 1 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 2
- Do
- Run `python3 main.py --step 2` online to publish six fictional readings to two sensor topics on the public broker.
- You will see
- Six `topic payload` lines show r1 through r6 arriving on living-room and kitchen topics before `Delivery check: six validated payloads reached the subscriber.`
- Why it matters
- The actual received topics and JSON establish which sensor identities and fields reached intake behind the final delivery count.

Step 2 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 3
- Do
- Run `python3 main.py --step 3` to validate a payload after deliberately removing its humidity field.
- You will see
- The displayed rejected JSON lacks humidity beside `Rule: every Reading field is required before storage.` and `Broken rule: required field check -> missing=humidity`.
- Why it matters
- Putting the malformed payload next to the violated rule lets the learner inspect why rejection is deliberate rather than a silent storage gap.

Step 3 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 4
- Do
- Run `python3 main.py --step 4` to insert the validated fixture into SQLite.
- You will see
- After the printed SELECT statement, a six-row table lists each event, sensor, observed time, and temperature; the duplicate rule names event_id as primary key.
- Why it matters
- The returned rows prove what was stored, while the identity rule explains how a replayed event would be constrained.

Step 4 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 5
- Do
- Run `python3 main.py --step 5` to query only the living-room sensor window.
- You will see
- The detail SELECT returns r1=20.0, r2=21.0, and r3=22.0; the aggregate SELECT then returns `3 21.0 20.0 22.0`.
- Why it matters
- Showing both SQL statements, source rows, and aggregate row lets the learner recompute the rollup and see what its average hides.

Step 5 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 6
- Do
- Run `python3 main.py --step 6` to add one reading whose observed time belongs inside the earlier window but whose receive time is later.
- You will see
- The late-1 row shows observed_at=105 and received_at=205 beside a before/after table changing 3 at 21.0 to 4 at 21.8.
- Why it matters
- The late row and repeated aggregate expose why the result changes and why consumers must receive the CORRECTED label.

Step 6 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 7
- Do
- Run `python3 main.py --step 7` to dry-run deletion of readings older than the cutoff; the transaction is rolled back.
- You will see
- The retention SELECT lists r1, r4, r2, and r5 as candidates, followed by the DELETE SQL and `Transaction action: ROLLBACK; no candidate was deleted.`
- Why it matters
- Showing candidate rows before the destructive statement lets the learner audit cutoff scope and confirm the dry run remained reversible.

Step 7 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 8
- Do
- Run `python3 main.py --step 8` to copy the database with SQLite's backup API and query the restored copy.
- You will see
- The source and restored SELECT statements each return 6, followed by `Comparison: row counts match, so the restored sample is readable.`
- Why it matters
- Running the same read against both databases demonstrates recoverability instead of treating a completed backup call as sufficient evidence.

Step 8 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
Chapter checks
These questions refer to the chapter’s examples. Use the return links to review their answers.
Which retention release criterion is strongest before deleting raw telemetry?
Return to the chapter’s knowledge checkWhy does a time-partitioned store use chunk- or partition-drop deletion instead of a large DELETE WHERE time < cutoff job?
Return to the chapter’s knowledge checkA team wants to shorten raw telemetry retention because storage is growing. What should they do before changing the deletion policy?
Return to the chapter’s knowledge check