Computing FoundationsSystems Foundations: Storage, Observability, and Tooling › Day 39

Hands-on lab — Day 39: Data Storage: Files, Databases, Object Storage, and Caches

Commands

Setup

cd labs/sections/computing-foundations/day-039-data-storage-files-databases-object-storage

Run

bash examples/storage_demo.sh
bash starter/storage_demo.sh

Test

bash tests/run_tests.sh

File tree

examples/storage_demo.sh
expected-output/FIELDS.md
expected-output/sample-macos.txt
expected-output/sample-tests.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/storage_demo.sh
starter/storage-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 039 lab — Store Data Four Ways

Lesson

Purpose

Day 39's lesson maps the four main ways software stores data. This lab makes them concrete: you store the same small dataset four ways — as a file, in a SQLite database, in a simulated object-storage bucket, and behind a cache — and feel exactly what each is good for. The centerpiece is watching a SQL query answer a precise question that a plain file could only grep at.

Learning objectives

  • Write structured data to a file and read it back.
  • Create a real SQLite database, insert rows, and run a SELECT with a WHERE clause and an aggregate.
  • Store a blob under a content key (its hash) in a bucket directory and retrieve it by that key — the object-storage model.
  • Implement a tiny key→value cache that shows a hit versus a recompute.
  • Explain, for a given workload, which of the four stores fits and why.

Prerequisites

  • The Day 39 lesson (read it first — it explains every store this lab builds).
  • Days 3 and 5 (the memory hierarchy; data as bytes).
  • A terminal with bash and sqlite3 (both preinstalled on macOS and most Linux). No programming experience required; every command is given.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, sqlite3 3.51.0).
  • Linux — fully supported (any distribution with bash, sqlite3, and shasum or sha256sum).
  • Windows — run the scripts unmodified inside WSL (Windows Subsystem for Linux); native PowerShell is not supported here.

Hardware requirements

Any computer made in roughly the last 15 years. The lab writes only a few kilobytes to a temporary directory and needs no special hardware.

Required software

  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • sqlite3 — the embedded database used in step 2. Preinstalled on macOS; on Debian/Ubuntu install with apt install sqlite3.
  • shasum or sha256sum (one is always present on macOS/Linux) for the content-key step.

Free and open-source options

Everything here is free. sqlite3 is public-domain software, bash and the hashing tools are open source or ship with your OS, and the object-storage and cache steps are simulated with plain directories and files. No account, API key, cloud service, or purchase is needed.

Installation

None beyond sqlite3, which is almost always already present:

cd labs/sections/computing-foundations/day-039-data-storage-files-databases-object-storage
sqlite3 --version   # confirm sqlite3 is installed

If that prints a version, you are ready. If not, install it (see Required software above).

File structure

day-039-data-storage-files-databases-object-storage/
├── README.md                     ← you are here
├── metadata.yml                  ← machine-readable lab metadata
├── starter/
│   ├── storage_demo.sh           ← YOUR working file (4 exercises)
│   └── storage-worksheet.md      ← record your query, key, and store choices
├── examples/
│   └── storage_demo.sh           ← completed reference implementation
├── tests/
│   └── run_tests.sh              ← automated checks (exits 0/non-zero)
├── expected-output/
│   ├── sample-macos.txt          ← real captured demo run (macOS)
│   ├── sample-tests.txt          ← real captured test run (macOS)
│   └── FIELDS.md                 ← required fields and platform notes
├── requirements/
│   └── README.md                 ← dependency statement
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first
bash examples/storage_demo.sh

## 2. Your task: complete the four exercises in the starter, then run it
bash starter/storage_demo.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/storage_demo.sh — runs the reference demo end to end: writes a CSV file; creates a SQLite database, inserts rows, and runs a SELECT ... WHERE state='OH' GROUP BY customer aggregate; stores a blob in a bucket directory under its SHA-256 content key and reads it back; and runs a key→value cache that prints MISS then HIT. It works in a temporary directory and cleans up on exit.
  • bash starter/storage_demo.sh — the same skeleton with four exercises left for you: create the table and insert rows, run the query, store and fetch a blob by key, and implement the cache's hit/miss logic. Each exercise comment names the exact command.
  • bash tests/run_tests.sh — drives the reference demo into an inspectable temp directory and verifies real behavior: the database file exists, the query returns ada|180.0 and grace|90.0, the blob is retrievable by a key equal to its content hash, and the cache prints MISS then HIT. Exits 0 on success, non-zero on failure, so it runs in CI.

Expected output

See expected-output/sample-macos.txt — a real captured run. The load-bearing lines:

[2/4] DATABASE (SQLite)
  Query: total revenue per customer in state 'OH'
  ada|180.0
  grace|90.0

[4/4] CACHE (key -> value with timestamp)
  First call:  MISS -> computed and stored: 270.0 (at 23:22:13)
  Second call: HIT  -> served from cache (no recompute): 270.0

Your object key is identical for identical bytes; the cache timestamp and the temp-dir path will differ. expected-output/FIELDS.md lists exactly which lines must appear and which values legitimately vary.

Validation steps

  1. Run bash examples/storage_demo.sh — it must exit without errors and print all four [n/4] sections.
  2. Confirm the database section prints ada|180.0 and grace|90.0.
  3. Confirm the cache section prints MISS then HIT.
  4. Complete the four exercises in starter/storage_demo.sh and run it — its output should match the example's.
  5. Run the tests (next section) — all checks must pass and the exit code must be 0.

Tests

bash tests/run_tests.sh

Expected final line: 7 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure. A real captured run is in expected-output/sample-tests.txt.

Cleanup

Nothing to clean up manually: every script does its work inside a temporary directory created with mktemp and removes it on exit (the test harness does the same). To reset your starter file, restore it from git: git checkout -- starter/storage_demo.sh.

Troubleshooting

See troubleshooting.md for the full list (sqlite3 not found, empty query results, the cache always missing, database is locked).

Security notes

See security.md. Short version: the scripts make no network calls, need no elevated privileges, and write only to a private temp directory — but treat SQL built from untrusted input, and any real secrets, with the care the file describes.

Extension exercises

  1. Turn the object step into real content-addressed storage: store the same bytes twice and confirm the second store reuses the same key (free deduplication); then store slightly different bytes and confirm a new key.
  2. Add a second table and a JOIN: a customers table plus the orders table, then one query listing each customer's name and total.
  3. Give the cache a time-to-live: stamp each cache file with a creation time and treat entries older than N seconds as a miss, so stale values expire.
  • Previous day: Day 38 — Regular Expressions (labs/sections/computing-foundations/day-038-regular-expressions/, to be written).
  • Next day: Day 40 — Observability: Logs, Metrics, Traces, and Dashboards (labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards/, to be written).

Expected output

FIELDS.md

# Expected output — required fields and platform notes

`sample-macos.txt` is a real captured run of `examples/storage_demo.sh` on
macOS (Apple Silicon). `sample-tests.txt` is a real captured run of
`tests/run_tests.sh`. Your own runs must contain the same load-bearing lines,
listed below; a few values legitimately vary.

## Lines that must appear (all platforms)

From `examples/storage_demo.sh`:

- `[1/4] FILE (structured CSV)` and the four CSV data rows.
- `[2/4] DATABASE (SQLite)` followed by the aggregated rows `ada|180.0` and
  `grace|90.0` — the total revenue per Ohio customer.
- `[3/4] OBJECT STORAGE (bucket + content key)` with a `PUT object under key:`
  line and the retrieved blob text `quarterly report: revenue up, storage bill down`.
- `[4/4] CACHE (key -> value with timestamp)` with `First call:  MISS` then
  `Second call: HIT`.

From `tests/run_tests.sh`: the final line `7 checks, 0 failure(s).` and exit
code 0.

## Values that legitimately differ

- **The object key** (`a185ba75...`) is the SHA-256 hash of the blob. It is
  identical on every machine for identical bytes — that is the point of
  content addressing — but if you edit the blob text, the key changes.
- **The cache timestamp** (`at 23:22:13`) is the wall-clock time of your run.
- **The work-dir path** printed on the second line is a temporary directory;
  the captured sample uses a fixed path only for readability. Real runs use a
  `mktemp` path under your system temp directory and clean it up on exit.

## Linux differences

The scripts are POSIX-portable and behave identically on Linux, using
`sha256sum` if `shasum` is absent (both produce the same SHA-256 key). The
only visible difference is the temp-dir path style (`/tmp/...`).

sample-macos.txt

=== Store Data Four Ways ===
Work dir: /tmp/storage_demo_sample

[1/4] FILE (structured CSV)
  Wrote /tmp/storage_demo_sample/orders.csv:
    id,customer,state,amount
    1,ada,OH,120.0
    2,grace,OH,90.0
    3,linus,CA,200.0
    4,ada,OH,60.0
  -> A file holds the raw rows, but can only be read whole or grepped.

[2/4] DATABASE (SQLite)
  Created table 'orders' and inserted 4 rows (with an index on state).
  Query: total revenue per customer in state 'OH'
  ada|180.0
  grace|90.0
  -> A SQL query answered a precise question that a file could only grep.

[3/4] OBJECT STORAGE (bucket + content key)
  PUT object under key: a185ba75c657f161ef2f12a220c33f422dc82b5d8c1a9eeac813da8a1cea5ad8
  GET object back by key:
    quarterly report: revenue up, storage bill down
  -> The bucket is flat; the key names the blob; we fetch it whole.

[4/4] CACHE (key -> value with timestamp)
  First call:  MISS -> computed and stored: 270.0 (at 23:22:13)
  Second call: HIT  -> served from cache (no recompute): 270.0
  -> The second call skipped the work. Delete the cache and nothing is
     lost — the real data still lives in the file, database, and bucket.

=== Done. Each store fit a different job. Scratch dir will be cleaned up. ===

sample-tests.txt

  ok: sqlite3 is installed
  ok: demo script exits successfully
  ok: SQLite database file was created
  ok: query returns expected OH rows (ada|180.0, grace|90.0)
  ok: blob is retrievable by key and key matches its content hash
  ok: cache prints MISS on the first call
  ok: cache prints HIT on the second call (no recompute)

7 checks, 0 failure(s).

Source files

examples/storage_demo.sh (5408 bytes)
#!/usr/bin/env bash
# Day 039 lab — "Store Data Four Ways" (completed reference implementation).
#
# Stores the SAME small dataset four ways and shows what each is good for:
#   1) FILE            — structured data as a CSV file you read back
#   2) DATABASE        — a SQLite database you QUERY (beats grepping a file)
#   3) OBJECT STORAGE  — a blob filed by a content-key (hash) in a bucket dir
#   4) CACHE           — a key->value file cache showing a HIT vs a recompute
#
# Offline, no network, no sudo. Requires: bash and sqlite3 (preinstalled on
# macOS and most Linux). Everything is created under a temporary work dir and
# cleaned up on exit.
set -euo pipefail

# --- Setup: a private scratch directory, removed automatically on exit -------
# The test harness may set STORAGE_DEMO_WORKDIR to a directory it wants to
# inspect afterward; in that case we use it and do NOT delete it. Normal runs
# get a fresh temp dir that is cleaned up on exit.
if [ -n "${STORAGE_DEMO_WORKDIR:-}" ]; then
  work="${STORAGE_DEMO_WORKDIR}"
  mkdir -p "${work}"
  trap - EXIT
else
  work="$(mktemp -d "${TMPDIR:-/tmp}/storage_demo.XXXXXX")"
  cleanup() { rm -rf "${work}"; }
  trap cleanup EXIT
fi

# Pick a SHA-256 tool that exists on both macOS and Linux.
if command -v shasum >/dev/null 2>&1; then
  sha256() { shasum -a 256 "$1" | awk '{print $1}'; }
elif command -v sha256sum >/dev/null 2>&1; then
  sha256() { sha256sum "$1" | awk '{print $1}'; }
else
  echo "Need shasum or sha256sum (both are standard on macOS/Linux)." >&2
  exit 1
fi

echo "=== Store Data Four Ways ==="
echo "Work dir: ${work}"
echo

# --- 1) FILE: structured data as CSV -----------------------------------------
# A file is the simplest store: named bytes on disk. Perfect for a small,
# single-writer dataset like this. We write it, then read it back.
echo "[1/4] FILE (structured CSV)"
orders_csv="${work}/orders.csv"
cat > "${orders_csv}" <<'CSV'
id,customer,state,amount
1,ada,OH,120.0
2,grace,OH,90.0
3,linus,CA,200.0
4,ada,OH,60.0
CSV
echo "  Wrote ${orders_csv}:"
sed 's/^/    /' "${orders_csv}"
echo "  -> A file holds the raw rows, but can only be read whole or grepped."
echo

# --- 2) DATABASE: SQLite you can QUERY ---------------------------------------
# The same data in a real relational database. We create a table, insert the
# rows, then run a SELECT with a WHERE and an aggregate (SUM ... GROUP BY) —
# a precise question a file could never answer without scanning everything.
echo "[2/4] DATABASE (SQLite)"
db="${work}/storage_demo.db"
sqlite3 "${db}" <<'SQL'
CREATE TABLE orders (
  id       INTEGER PRIMARY KEY,
  customer TEXT NOT NULL,
  state    TEXT NOT NULL,
  amount   REAL NOT NULL
);
INSERT INTO orders (customer, state, amount) VALUES
  ('ada',   'OH', 120.0),
  ('grace', 'OH',  90.0),
  ('linus', 'CA', 200.0),
  ('ada',   'OH',  60.0);
CREATE INDEX idx_orders_state ON orders(state);
SQL
echo "  Created table 'orders' and inserted 4 rows (with an index on state)."
echo "  Query: total revenue per customer in state 'OH'"
sqlite3 "${db}" \
  "SELECT customer, SUM(amount) AS total
     FROM orders
    WHERE state = 'OH'
    GROUP BY customer
    ORDER BY customer;" | sed 's/^/  /'
echo "  -> A SQL query answered a precise question that a file could only grep."
echo

# --- 3) OBJECT STORAGE: a blob filed by content-key --------------------------
# Object storage keeps whole blobs in a flat "bucket", each under a key. Here
# the key is the blob's own SHA-256 hash (content-addressed): identical bytes
# always get the same key, and you fetch the blob back by that key.
echo "[3/4] OBJECT STORAGE (bucket + content key)"
bucket="${work}/bucket"
mkdir -p "${bucket}"
blob_src="${work}/report.txt"
printf 'quarterly report: revenue up, storage bill down\n' > "${blob_src}"
key="$(sha256 "${blob_src}")"
cp "${blob_src}" "${bucket}/${key}"
echo "  PUT object under key: ${key}"
echo "  GET object back by key:"
cat "${bucket}/${key}" | sed 's/^/    /'
echo "  -> The bucket is flat; the key names the blob; we fetch it whole."
echo

# --- 4) CACHE: key -> value with a HIT vs recompute --------------------------
# A cache is a fast, disposable copy in front of slow work. compute() pretends
# to be expensive. The cache stores results by key; a second call for the same
# key is served from the cache (a HIT) instead of recomputing.
echo "[4/4] CACHE (key -> value with timestamp)"
cache_dir="${work}/cache"
mkdir -p "${cache_dir}"

expensive_total_for_state() {
  # Pretend this is slow (e.g. a big scan). It queries the DB for a state total.
  sqlite3 "${db}" "SELECT COALESCE(SUM(amount),0) FROM orders WHERE state='$1';"
}

cached_total_for_state() {
  local state="$1"
  local cache_file="${cache_dir}/total_${state}"
  if [ -f "${cache_file}" ]; then
    echo "HIT  -> served from cache (no recompute): $(cat "${cache_file}")"
  else
    local value; value="$(expensive_total_for_state "${state}")"
    printf '%s' "${value}" > "${cache_file}"
    echo "MISS -> computed and stored: ${value} (at $(date '+%H:%M:%S'))"
  fi
}

echo "  First call:  $(cached_total_for_state OH)"
echo "  Second call: $(cached_total_for_state OH)"
echo "  -> The second call skipped the work. Delete the cache and nothing is"
echo "     lost — the real data still lives in the file, database, and bucket."
echo

echo "=== Done. Each store fit a different job. Scratch dir will be cleaned up. ==="
metadata.yml (615 bytes)
lesson_id: D039
day: 39
kind: command-line-inspection
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-039-data-storage-files-databases-object-storage
run_commands:
  - bash examples/storage_demo.sh
  - bash starter/storage_demo.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - '# nothing to clean: scripts use a temp dir removed automatically on exit'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), sqlite3 3.51.0, bash tests/run_tests.sh → 7 checks, 0 failure(s), exit 0'
requirements/README.md (906 bytes)
# Dependencies — Day 039 lab

This lab needs only a POSIX shell and the `sqlite3` command-line tool.

- **`bash`** ≥ 3.2 — preinstalled on macOS and every mainstream Linux
  distribution.
- **`sqlite3`** — the embedded relational database used in step 2.
  - **macOS:** preinstalled. Confirm with `sqlite3 --version`.
  - **Debian/Ubuntu:** `sudo apt install sqlite3` if it is missing.
  - **Fedora/RHEL:** `sudo dnf install sqlite`.
  - **Windows:** use WSL and install `sqlite3` inside it as on Linux.
- **`shasum` or `sha256sum`** — for the object-storage content key. One of
  these is always present on macOS and Linux; the scripts pick whichever
  exists, and both produce the same SHA-256 value.

There is deliberately no `requirements.txt`/`package.json`: the lab installs
nothing beyond the `sqlite3` package above and runs fully offline. No network
access and no API keys are required.
starter/storage_demo.sh (3503 bytes)
#!/usr/bin/env bash
# Day 039 lab — "Store Data Four Ways" (YOUR working file).
#
# Complete the FOUR numbered exercises below. Each one names the exact command
# to use. The finished reference is in examples/storage_demo.sh — try to do it
# yourself first, then compare. Run this file with:  bash starter/storage_demo.sh
#
# Offline, no network, no sudo. Requires: bash and sqlite3.
set -euo pipefail

work="$(mktemp -d "${TMPDIR:-/tmp}/storage_demo.XXXXXX")"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT

if command -v shasum >/dev/null 2>&1; then
  sha256() { shasum -a 256 "$1" | awk '{print $1}'; }
elif command -v sha256sum >/dev/null 2>&1; then
  sha256() { sha256sum "$1" | awk '{print $1}'; }
else
  echo "Need shasum or sha256sum." >&2; exit 1
fi

echo "=== Store Data Four Ways (starter) ==="
echo "Work dir: ${work}"
echo

# --- 1) FILE (given, already works) ------------------------------------------
echo "[1/4] FILE (structured CSV)"
orders_csv="${work}/orders.csv"
cat > "${orders_csv}" <<'CSV'
id,customer,state,amount
1,ada,OH,120.0
2,grace,OH,90.0
3,linus,CA,200.0
4,ada,OH,60.0
CSV
echo "  Wrote ${orders_csv}"
echo

# --- 2) DATABASE -------------------------------------------------------------
echo "[2/4] DATABASE (SQLite)"
db="${work}/storage_demo.db"

# Exercise 1: CREATE the 'orders' table and INSERT the four rows.
# Use sqlite3 with a heredoc. Columns: id INTEGER PRIMARY KEY, customer TEXT,
# state TEXT, amount REAL. Insert the same four rows as the CSV above.
# Replace the line below with your sqlite3 command.
#   sqlite3 "${db}" <<'SQL'  ... SQL
echo "  (exercise 1: create table + insert rows)"

# Exercise 2: run a SELECT with a WHERE and an aggregate. Print total revenue
# per customer in state 'OH', grouped by customer, ordered by customer:
#   sqlite3 "${db}" "SELECT customer, SUM(amount) AS total FROM orders
#                    WHERE state='OH' GROUP BY customer ORDER BY customer;"
echo "  Query result (exercise 2):"
echo "  (exercise 2: run the SELECT here)"
echo

# --- 3) OBJECT STORAGE -------------------------------------------------------
echo "[3/4] OBJECT STORAGE (bucket + content key)"
bucket="${work}/bucket"; mkdir -p "${bucket}"
blob_src="${work}/report.txt"
printf 'quarterly report: revenue up, storage bill down\n' > "${blob_src}"

# Exercise 3: store the blob under a content key (its SHA-256 hash), then fetch
# it back by key. Compute the key with:  key="$(sha256 "${blob_src}")"
# Then copy the blob into the bucket:     cp "${blob_src}" "${bucket}/${key}"
# Then read it back:                      cat "${bucket}/${key}"
echo "  (exercise 3: put blob by key, then get it back)"
echo

# --- 4) CACHE ----------------------------------------------------------------
echo "[4/4] CACHE (key -> value)"
cache_dir="${work}/cache"; mkdir -p "${cache_dir}"

expensive_total_for_state() {
  sqlite3 "${db}" "SELECT COALESCE(SUM(amount),0) FROM orders WHERE state='$1';"
}

cached_total_for_state() {
  local state="$1"
  local cache_file="${cache_dir}/total_${state}"
  # Exercise 4: implement the cache. If "${cache_file}" exists, print
  #   "HIT  -> $(cat "${cache_file}")"
  # otherwise compute value="$(expensive_total_for_state "${state}")", save it
  # to "${cache_file}", and print "MISS -> ${value}".
  echo "MISS -> (exercise 4: implement HIT/MISS)"
}

echo "  First call:  $(cached_total_for_state OH)"
echo "  Second call: $(cached_total_for_state OH)"
echo

echo "=== Done. Compare with examples/storage_demo.sh. ==="
starter/storage-worksheet.md (1145 bytes)
# Storage worksheet — Day 039

Fill this in as you complete `starter/storage_demo.sh` and read the lesson.

## 1. Your database query and its result

Write the exact `SELECT` you ran in Exercise 2 (the OH revenue-per-customer query):

```sql
-- paste your query here
```

Paste its result (one row per Ohio customer):

```text
-- paste the output rows here (e.g. ada|180.0)
```

One sentence: why can the database answer this precise question faster than
grepping the CSV file, once the table has many rows?

> _your answer_

## 2. The object key you generated

Paste the content key (SHA-256 hash) your blob was stored under in Exercise 3:

```text
-- paste the key here
```

One sentence: if you store the exact same bytes again, what key do they get,
and why does that give you deduplication for free?

> _your answer_

## 3. Which store would you pick, and why?

| Workload | Store you'd pick | Why (one sentence) |
| --- | --- | --- |
| A 5 GB training dataset | | |
| A user profile record (name, email, settings) | | |
| A hot counter incremented thousands of times per second | | |

## 4. One thing that surprised you

> _your answer_
tests/run_tests.sh (3171 bytes)
#!/usr/bin/env bash
# Tests for the Day 039 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Drives the completed reference demo into an inspectable work directory and
# checks real behavior of all four stores:
#   - the SQLite database file is created
#   - the SELECT ... WHERE ... GROUP BY returns the expected OH rows
#   - the object blob is retrievable from the bucket by its content key
#   - the cache returns a HIT on the second call for the same key
# No network. Exits 0 on success, non-zero on any failure.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
demo="${lab_dir}/examples/storage_demo.sh"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

# Pre-flight: sqlite3 must exist (the lab's whole point).
if command -v sqlite3 >/dev/null 2>&1; then
  check "sqlite3 is installed" "yes"
else
  check "sqlite3 is installed" "no"
  echo
  echo "${checks} checks, ${failures} failure(s)."
  echo "Install sqlite3 first (macOS: preinstalled; Debian/Ubuntu: apt install sqlite3)."
  exit 1
fi

# Run the reference demo into a known, inspectable work dir (no network).
work="$(mktemp -d "${TMPDIR:-/tmp}/storage_demo_test.XXXXXX")"
trap 'rm -rf "${work}"' EXIT
output="$(STORAGE_DEMO_WORKDIR="${work}" bash "${demo}" 2>&1)"
rc=$?
check "demo script exits successfully" "$([ ${rc} -eq 0 ] && echo yes || echo no)"

# 1) DATABASE: file created and the query returns the expected aggregated rows.
check "SQLite database file was created" "$([ -f "${work}/storage_demo.db" ] && echo yes || echo no)"

query_out="$(sqlite3 "${work}/storage_demo.db" \
  "SELECT customer, SUM(amount) FROM orders WHERE state='OH' GROUP BY customer ORDER BY customer;" 2>/dev/null)"
expected_query="ada|180.0
grace|90.0"
check "query returns expected OH rows (ada|180.0, grace|90.0)" \
  "$([ "${query_out}" = "${expected_query}" ] && echo yes || echo no)"

# 2) OBJECT STORAGE: the blob is retrievable from the bucket by its key, and
#    re-hashing the retrieved bytes reproduces that same key (content-addressed).
if command -v shasum >/dev/null 2>&1; then
  hasher() { shasum -a 256 "$1" | awk '{print $1}'; }
else
  hasher() { sha256sum "$1" | awk '{print $1}'; }
fi
stored_key="$(ls "${work}/bucket" 2>/dev/null | head -1)"
if [ -n "${stored_key}" ] && [ -f "${work}/bucket/${stored_key}" ]; then
  rehash="$(hasher "${work}/bucket/${stored_key}")"
  check "blob is retrievable by key and key matches its content hash" \
    "$([ "${rehash}" = "${stored_key}" ] && echo yes || echo no)"
else
  check "blob is retrievable by key and key matches its content hash" "no"
fi

# 3) CACHE: the printed output shows a MISS then a HIT for the same key.
echo "${output}" | grep -q "First call:  MISS" && miss=yes || miss=no
echo "${output}" | grep -q "Second call: HIT"  && hit=yes  || hit=no
check "cache prints MISS on the first call" "${miss}"
check "cache prints HIT on the second call (no recompute)" "${hit}"

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 039 lab

  • sqlite3: command not found. The database step needs the sqlite3 tool. It ships on macOS; on Debian/Ubuntu run sudo apt install sqlite3, on Fedora sudo dnf install sqlite. Confirm with sqlite3 --version.

  • The query returns nothing. Check that your WHERE clause matches the data exactly — the state code is 'OH', not 'Ohio' — and that the CREATE TABLE/INSERT ran before the SELECT. In the starter, that means finishing Exercise 1 before Exercise 2.

  • Error: no such table: orders. The inserts did not run, usually because Exercise 1 is still a placeholder or the heredoc was mistyped. Copy the CREATE TABLE ... INSERT ... block from examples/storage_demo.sh and compare.

  • The cache always prints MISS. Two common causes: the cache filename changes between calls (it must be stable for a given key), or the result is saved after the existence check instead of only on a miss. The correct order is: check for the file first; if present, print a HIT; otherwise compute, save, and print a MISS.

  • database is locked. Another process still holds storage_demo.db. Close any other sqlite3 session on that file. Because the scripts use a fresh temp directory each run, simply rerunning usually clears it.

  • shasum/sha256sum not found. Rare, but if neither is present the scripts stop with a clear message. Install coreutils (Linux: sudo apt install coreutils) or Perl's shasum (usually already present).

  • unbound variable errors when editing. The scripts run with set -euo pipefail, so referencing an unset variable stops the run immediately. Declare each local variable on its own line and set every variable before you use it.

  • Windows. Native PowerShell is not supported. Install WSL, open a Linux shell, and run the commands exactly as on Linux.

Security notes

Security notes — Day 039 lab

  • What the scripts do: create a small CSV file, a SQLite database, a "bucket" directory, and a cache directory — all inside a private temporary directory made with mktemp and removed on exit. They make no network connections, need no elevated privileges, and write nothing outside that temp directory.

  • Temp dir only. Because everything lives under a per-run temp directory, nothing you create here persists or leaks into the rest of your system. If you adapt the scripts to keep data, write it somewhere you control and clean up deliberately.

  • Never store secrets unencrypted. This lab stores only harmless sample data. In real systems, never keep passwords, API keys, or personal data as plaintext in a file or database. Sensitive data should be encrypted at rest, and secrets belong in a dedicated secrets manager, never in a committed file.

  • SQL injection (conceptual). The demo builds SQL with fixed, trusted values only. In real applications, never paste untrusted input (a form field, a URL parameter) directly into a SQL string — an attacker can craft input that rewrites your query. The fix is parameterized queries, where the database treats supplied values strictly as data and never as commands. Keep this rule in mind the moment your queries include anything a user typed.

  • Read before running. Both scripts are short and commented — read them first. Running unread shell scripts is a common way to get compromised; the course's rule is that every lab script is small enough to read and understand before executing.