Working With SQLite From an Application
SQLite
Chapter 8 · Working With SQLite From an Application
sqlite1-1 and sqlite1-2 described the "no connection ceremony" workflow. This chapter shows it in real, working application code, in two languages, so the simplicity claim is demonstrated rather than just asserted.
Python's Built-in sqlite3 Module
SQLite support is genuinely built into Python's own standard library — no pip install required at all. This is a real, distinctive fact worth naming: neither MySQL nor Postgres connectivity ships in Python's own standard library; both require a third-party package (mysql-connector-python, psycopg2) just to connect.
import sqlite3
with sqlite3.connect("notes.db") as conn:
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
# Parameterized — never string-concatenate user input into SQL
cursor.execute("INSERT INTO notes (body) VALUES (?)", ("First real note",))
conn.commit()
cursor.execute("SELECT * FROM notes")
for row in cursor.fetchall():
print(row)
The parameterized execute(sql, (param,)) form is deliberate — a direct callback to sqli1's own parameterization material and postgres1-8's own dynamic-SQL-injection warning. That lesson applies here unchanged, in a completely different language and context. The with sqlite3.connect(...) context-manager form is the idiomatic Python way to guarantee the connection is properly committed and closed.
Node.js's better-sqlite3
better-sqlite3 is a real, popular SQLite binding for Node — and it's deliberately, notably synchronous, in contrast to Node's usual async-everything idioms. The library's own documented reasoning: SQLite operations are so fast that the overhead of async/await machinery genuinely isn't worth it for typical use — a real design decision, not an oversight, and a direct, concrete instance of sqlite1-1's own "near-instant, no handshake" claim.
const Database = require('better-sqlite3');
const db = new Database('notes.db');
db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');
// Parameterized here too
db.prepare('INSERT INTO notes (body) VALUES (?)').run('First real note');
const notes = db.prepare('SELECT * FROM notes').all();
console.log(notes);Side-by-Side — The Simplicity Gap
| MySQL/Postgres connection config | SQLite connection config |
|---|---|
| Host, port | A single file path string |
| Username, password | |
| Database name | |
| Connection pool size | |
| SSL/TLS configuration | |
| A separate driver package to install |
Closing Connections & Resource Management
Even though there's no network connection to manage, a SQLite connection object still wraps real operating-system file handles and should still be explicitly closed — via a context manager, a try/finally, or an explicit close() call — when done. It's easy to assume this doesn't matter "since it's just a file," but proper cleanup still matters, especially in the long-running server process context sqlite1-6 covered.
sqli1's and postgres1-8's own material applies unchanged. String-concatenating input directly into a SQL string is just as exploitable in a SQLite-backed CLI tool or desktop app as in a networked web application, especially if that "local" input actually originates from an untrusted source — a file the application opens, a value a user pastes in, or data synced in from elsewhere. The absence of a network-facing attack surface doesn't mean the absence of the underlying vulnerability class.
sqlite1-1's roadmap. sqlite1-9 covers genuine limitations honestly, before the capstone applies all of this together.
Hands-On Exercises
Explain why Python's built-in sqlite3 module being part of the standard library (unlike MySQL/Postgres connectivity) is itself a small but real reflection of this course's own throughline.
📄 View solutionExplain why better-sqlite3's deliberately synchronous design is a genuine, documented choice rather than an oversight, tying your answer back to sqlite1-1's own "near-instant" framing.
📄 View solutionUsing this chapter's own warn-box, explain why SQL injection remains exactly as real a risk in a SQLite-backed application as in a networked MySQL/Postgres application, even without a network-facing attack surface — give a concrete example of an "untrusted local input" source.
📄 View solutionChapter 8 Quick Reference
- Python's
sqlite3is built into the standard library — no separate driver install, unlike MySQL/Postgres connectivity - Always parameterize (
execute(sql, (param,))/db.prepare(sql).run(param)) — sqli1's/postgres1-8's own lesson applies unchanged - better-sqlite3 is deliberately synchronous — SQLite operations are fast enough that async overhead isn't worth it, per the library's own documented reasoning
- Connection setup: MySQL/Postgres need host/port/user/password/pool/SSL config; SQLite needs a file path string
- Connections still wrap real file handles and should still be explicitly closed/managed
- SQL injection risk is unchanged by the absence of a network-facing surface — untrusted local input (files, user-pasted values, synced data) is still a real vector
- Next chapter: Limitations & Gotchas