Chapter 20 · Production Capstone: Build and Operate a Complete SQLite Application Database

Test, Benchmark, Back Up, Restore, and Secure the Database

Turn the FieldNotes design into an operationally testable artifact with constraint, transaction, migration and concurrency tests, evidence-backed benchmarks, SQLite-aware backup/restore, integrity checks, and a security review.

Beginner180–240 minutesAutomated tests + benchmark + backup/restore drillSQLite 3.53.4 baselineFile-backed tests required for WAL/locking/backup semanticsLast reviewed: August 2026

Production readiness is a report of evidence

A database that survives a demo is not yet an operated system. This lesson builds one acceptance suite that covers schema, constraints, transactions, migrations, concurrency, hot query plans, backup/restore, integrity, and security. The output is a pass/fail operational report with enough environment metadata to reproduce failures.

01

Automate tests for schema shape, constraints, migrations, transaction atomicity, expected queries, and two-connection contention.

02

Benchmark representative reads and writes while recording SQLite/runtime/configuration/dataset and query plans.

03

Improve only evidence-backed bottlenecks rather than applying a PRAGMA recipe.

04

Create a live SQLite-aware backup, open it independently, run checks, and perform a restore drill.

05

Combine quick_check, integrity_check, foreign_key_check, and domain-specific consistency queries.

06

Review file permissions, bound parameters, extension loading, sensitive fields, backups, and logs against explicit pass/fail criteria.

Test matrix: each layer can fail differently

Test layerExample assertionWhy it matters
Schemauser_version=2; required tables/indexes/view/trigger exist.Catches missing/partial migrations.
ConstraintOrphan device and malformed JSON are rejected.Proves invariants across every writer.
TransactionInjected failure leaves inspection/outbox counts unchanged.Proves atomicity.
IdempotencySame request_id twice yields one inspection + one event.Makes retry safe after uncertain client outcomes.
QueryDevice history returns expected rows/order.Prevents “fast but wrong” optimization.
ConcurrencySecond writer gets BUSY while first holds write lock; succeeds after release.Tests real file semantics.
Migrationv1 fixture migrates to v2 once; newer DB is refused.Prevents release drift.
Backup/restoreBackup independently opens/checks and restored domain counts match.Proves recovery, not just backup creation.

Representative Python tests

The course test suite should use temporary files for migration, WAL, locking, backup and restore tests. :memory: remains useful for isolated SQL logic that does not depend on file lifetime or multiple independent connections.

python · constraint + atomicity tests
import sqlite3import tempfilefrom pathlib import Pathwith tempfile.TemporaryDirectory() as td:    db = Path(td) / "fieldnotes.sqlite"    con = open_db(db)    migrate(con)    con.execute("INSERT INTO site(site_code,site_name) VALUES('PLANT-A','Plant A')")    con.execute("INSERT INTO device(site_id,device_code,device_name) VALUES(1,'P-007','Pump 007')")    # Invalid JSON must be rejected by the database, not only the UI.    try:        con.execute("UPDATE device SET metadata_json=? WHERE device_id=1", ("{bad",))        raise AssertionError("invalid JSON was accepted")    except sqlite3.IntegrityError:        pass    before = con.execute("SELECT count(*) FROM inspection").fetchone()[0]    con.execute("BEGIN IMMEDIATE")    try:        con.execute("""          INSERT INTO inspection(device_id,request_id,technician_name,                                 started_at,outcome,summary)          VALUES(1,'inject-1','Tech A','2026-08-12T08:00:00Z','pass','demo')        """)        raise RuntimeError("injected")    except RuntimeError:        con.execute("ROLLBACK")    after = con.execute("SELECT count(*) FROM inspection").fetchone()[0]    assert after == before    con.close()

Idempotency test

python · same business request twice
kwargs = dict(    request_id="req-0001",    device_id=1,    technician_name="Tech A",    started_at="2026-08-12T08:00:00Z",    finished_at="2026-08-12T08:10:00Z",    outcome="follow_up",    summary="seal temperature elevated",    measurements={"temperature_c": 83.2},    severity="warning",)first, created1 = record_inspection(con, **kwargs)second, created2 = record_inspection(con, **kwargs)assert created1 is Trueassert created2 is Falseassert first["inspection_id"] == second["inspection_id"]assert con.execute(    "SELECT count(*) FROM inspection WHERE request_id='req-0001'").fetchone()[0] == 1assert con.execute(    "SELECT count(*) FROM sync_outbox WHERE event_id='inspection:req-0001'").fetchone()[0] == 1

Benchmark record: no naked timing number

Before a timing result can support a change, record the environment and plan. Repeat trials, separate setup from the measured operation, and report distributions rather than one lucky run.

sql · benchmark metadata
SELECT sqlite_version();SELECT sqlite_source_id();PRAGMA journal_mode;PRAGMA synchronous;PRAGMA page_size;PRAGMA cache_size;PRAGMA mmap_size;PRAGMA compile_options;SELECT count(*) AS devices FROM device;SELECT count(*) AS inspections FROM inspection;SELECT count(*) AS notes FROM maintenance_note;EXPLAIN QUERY PLANSELECT inspection_id, started_at, outcome, summaryFROM inspectionWHERE device_id=?ORDER BY started_at DESCLIMIT 20;
python · host-language repeated timing
from statistics import medianfrom time import perf_counterdef time_recent_history(con, device_id, trials=25):    samples = []    for _ in range(trials):        t0 = perf_counter()        rows = con.execute(            """SELECT inspection_id, started_at, outcome, summary               FROM inspection               WHERE device_id=?               ORDER BY started_at DESC LIMIT 20""",            (device_id,),        ).fetchall()        samples.append(perf_counter() - t0)    return {"rows": len(rows), "median_s": median(samples),            "min_s": min(samples), "max_s": max(samples)}

Evidence-backed improvement example

Temporarily drop idx_inspection_device_started on a disposable benchmark copy, verify that the plan/timing worsens on a representative distribution, recreate it, run PRAGMA optimize when appropriate, and verify the plan/timing again. Do not run destructive benchmark DDL against the only production copy.

sql · before/after planner evidence on disposable copy
DROP INDEX idx_inspection_device_started;EXPLAIN QUERY PLANSELECT inspection_id, started_at, outcomeFROM inspectionWHERE device_id=17ORDER BY started_at DESC LIMIT 20;CREATE INDEX idx_inspection_device_startedON inspection(device_id, started_at DESC);PRAGMA optimize;EXPLAIN QUERY PLANSELECT inspection_id, started_at, outcomeFROM inspectionWHERE device_id=17ORDER BY started_at DESC LIMIT 20;

SQLite-aware live backup

Python's Connection.backup() wraps SQLite's Online Backup API. It produces a transactionally consistent destination while the source can remain live. Use a unique temporary destination, verify it, then atomically promote/rename according to the host filesystem's supported semantics. Do not overwrite the last known-good backup before the new candidate passes checks.

python · create and verify a backup candidate
from datetime import datetime, timezonefrom pathlib import Pathimport sqlite3stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")backup_dir = Path("backups")backup_dir.mkdir(exist_ok=True)candidate = backup_dir / f"fieldnotes-{stamp}.sqlite.partial"final = backup_dir / f"fieldnotes-{stamp}.sqlite"source = open_db("fieldnotes.sqlite")dest = sqlite3.connect(candidate, autocommit=True)source.backup(dest)dest.close(); source.close()check = sqlite3.connect(candidate, autocommit=True)assert check.execute("PRAGMA quick_check").fetchone()[0] == "ok"assert check.execute("PRAGMA integrity_check").fetchone()[0] == "ok"assert check.execute("PRAGMA foreign_key_check").fetchall() == []expected = check.execute("SELECT count(*) FROM inspection").fetchone()[0]check.close()candidate.replace(final)print(final, "verified inspections=", expected)

Restore drill, not restore theory

A restore drill creates a separate target, opens it with the same runtime policy, checks migration/version compatibility, runs structural and domain checks, and exercises a representative read. Only then can the runbook claim a tested recovery path.

sql · restore acceptance queries
PRAGMA user_version;PRAGMA quick_check;PRAGMA integrity_check;PRAGMA foreign_key_check;SELECT count(*) FROM site;SELECT count(*) FROM device;SELECT count(*) FROM inspection;SELECT count(*) FROM maintenance_note;SELECT count(*) FROM sync_outbox WHERE delivered_at IS NULL;-- Domain consistency: every inspection device must be queryable through FK.SELECT count(*) AS orphan_inspectionsFROM inspection AS iLEFT JOIN device AS d ON d.device_id=i.device_idWHERE d.device_id IS NULL;-- Idempotency should remain unique after restore.SELECT request_id, count(*)FROM inspectionGROUP BY request_idHAVING count(*) > 1;

Security acceptance review

ControlPass evidence
SQL injectionRepository tests store SQL-looking values as data; no value concatenation; dynamic identifiers are allowlisted.
File authorityDB directory, WAL/journal files and backup destination have reviewed OS permissions.
Extension loadingCore application does not enable arbitrary native extension loading.
trusted_schemaOFF established on application connections and tested with required schema.
Sensitive dataLogs avoid note bodies/measurement payloads unless explicitly redacted/approved.
SecretsRemote sync credentials are outside database and backup files; rotation/recovery owner documented.
Backup confidentialityBackup ACL/transport/retention match primary-data sensitivity.
Runtime patchingQualified current SQLite build is inventoried; minimum feature version is not mistaken for patch policy.

Operational acceptance report

text · pass/fail release report
FieldNotes Release Acceptance=============================Application version: __________________SQLite version/source id: _____________Compile/capability snapshot attached: [ ]DB user_version: ______________________Correctness[ ] schema manifest matches release[ ] constraint-negative tests pass[ ] idempotency replay test passes[ ] transaction failure injection leaves no partial state[ ] concurrency test reproduces + recovers from BUSYHealth[ ] quick_check = ok[ ] integrity_check = ok[ ] foreign_key_check = no rows[ ] domain consistency queries = expectedPerformance (target device)[ ] hot-query plans recorded[ ] read/write benchmark dataset recorded[ ] product latency/throughput SLOs met[ ] no unexplained busy/retry exhaustionRecovery[ ] online backup candidate verified[ ] restore drill completed[ ] restored counts/domain checks match policy[ ] off-device copy/retention verifiedSecurity[ ] parameter binding review pass[ ] file/backup permission review pass[ ] trusted_schema/extension policy pass[ ] logs/secrets review passRelease decision: PASS / FAILOwner: __________________ Date: _______

Acceptance checkpoint

Would you release this database?

A green happy-path test is not enough.

  1. Why must a backup be opened and checked independently?
  2. Why is foreign_key_check separate from integrity_check?
  3. Why should benchmark DDL run on a disposable copy?
  4. What environment facts make a timing result reproducible?
  5. Why is a successful single-session test insufficient for concurrency?
  6. What is the difference between a minimum SQLite feature version and a patch qualification policy?
Review the answers

A created backup can still be incomplete, misplaced, incompatible, or logically wrong; independent validation and restore prove recovery. integrity_check does not report foreign-key violations. Benchmark experiments can alter schema/data and should not endanger the source of truth. Record SQLite/source ID, PRAGMAs, dataset/distribution, plan, hardware/filesystem/cache context and repeated metrics. Locking/WAL semantics require multiple file-backed connections. The minimum version states required features; patch policy says which currently supported builds are approved for security/reliability fixes.

Bridge to operations

Lesson 5 assumes the release has passed this report. The final task is to operate it predictably when reality breaks the happy path: busy writers, slow plans, WAL growth, failed migrations, misconfigured foreign keys, storage errors, and damaged backup candidates.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.