Skip to content

Commit 1ee375f

Browse files
travisjneumanclaude
andcommitted
feat: add Module 06 — Databases & ORM curriculum
Five projects covering sqlite3 basics, SQLAlchemy declarative models, CRUD operations with an interactive CLI, Alembic schema migrations, and query optimization (N+1 problem, indexes, bulk operations). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2a45cd2 commit 1ee375f

22 files changed

Lines changed: 1885 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Module 06 / Project 01 — SQLite Basics
2+
3+
Home: [README](../../../../README.md) · Module: [Databases & ORM](../README.md)
4+
5+
## Focus
6+
7+
- Python's built-in `sqlite3` module (no install needed)
8+
- Creating tables with `CREATE TABLE`
9+
- Inserting rows with `INSERT INTO`
10+
- Querying data with `SELECT` and `WHERE`
11+
- Parameterized queries to prevent SQL injection
12+
13+
## Why this project exists
14+
15+
Before you learn an ORM like SQLAlchemy, you should understand what it does under the hood. SQLite ships with Python and requires no server setup. This project teaches you raw SQL through Python so that when you see ORM code later, you understand what it translates to.
16+
17+
## Run
18+
19+
```bash
20+
cd projects/modules/06-databases-orm/01-sqlite-basics
21+
python project.py
22+
```
23+
24+
This creates a `data/books.db` file. Delete it and re-run to start fresh.
25+
26+
## Expected output
27+
28+
```text
29+
--- Creating database and table ---
30+
Table 'books' created.
31+
32+
--- Inserting sample books ---
33+
Inserted 6 books.
34+
35+
--- All books ---
36+
1 | The Pragmatic Programmer | David Thomas | 1999
37+
2 | Clean Code | Robert C. Martin | 2008
38+
3 | Fluent Python | Luciano Ramalho | 2015
39+
4 | Python Crash Course | Eric Matthes | 2015
40+
5 | Design Patterns | Gang of Four | 1994
41+
6 | Refactoring | Martin Fowler | 1999
42+
43+
--- Books by year 2015 ---
44+
3 | Fluent Python | Luciano Ramalho | 2015
45+
4 | Python Crash Course | Eric Matthes | 2015
46+
47+
--- Books with 'Python' in the title ---
48+
3 | Fluent Python | Luciano Ramalho | 2015
49+
4 | Python Crash Course | Eric Matthes | 2015
50+
51+
--- Parameterized query demo ---
52+
Searching for author: Robert C. Martin
53+
2 | Clean Code | Robert C. Martin | 2008
54+
55+
--- Dangerous input safely handled ---
56+
Searching for: '; DROP TABLE books; --
57+
No books found (and the table still exists!).
58+
```
59+
60+
## Alter it
61+
62+
1. Add a `genre` column to the books table. Insert books with genres and query by genre.
63+
2. Add an `UPDATE` statement that changes a book's year. Verify it worked with a `SELECT`.
64+
3. Add a `DELETE` statement that removes a book by ID. Print the table before and after.
65+
66+
## Break it
67+
68+
1. Use string formatting (`f"... WHERE author = '{author}'"`) instead of parameterized queries. Pass in `'; DROP TABLE books; --` and see what happens.
69+
2. Try to insert a row with the wrong number of values. Read the error message.
70+
3. Remove the `conn.commit()` call after inserts. Query the data — is it there? Restart and check again.
71+
72+
## Fix it
73+
74+
1. Replace the string-formatted query with a parameterized one (`?` placeholders).
75+
2. Add `try/except` around database operations to handle `sqlite3.Error` gracefully.
76+
3. Use a context manager (`with conn:`) to ensure commits happen automatically.
77+
78+
## Explain it
79+
80+
1. What is SQL injection and why are parameterized queries the fix?
81+
2. What does `conn.commit()` do? What happens to your data without it?
82+
3. What is the difference between `conn.execute()` and `cursor.execute()`?
83+
4. Why does SQLite not need a separate server process?
84+
85+
## Mastery check
86+
87+
You can move on when you can:
88+
- create a table and insert rows using `sqlite3`,
89+
- write `SELECT` queries with `WHERE` clauses,
90+
- explain why parameterized queries matter,
91+
- use `conn.commit()` and understand transactions.
92+
93+
## Next
94+
95+
[Project 02 — SQLAlchemy Models](../02-sqlalchemy-models/)

projects/modules/06-databases-orm/01-sqlite-basics/data/.gitkeep

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Notes — SQLite Basics
2+
3+
## What I learned
4+
5+
6+
## What confused me
7+
8+
9+
## What I want to explore next
10+
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"""
2+
SQLite Basics — Raw SQL with Python's built-in sqlite3 module.
3+
4+
This project creates a small books database, inserts sample data,
5+
and demonstrates different ways to query it. The key lesson is
6+
parameterized queries: never build SQL strings with f-strings or
7+
.format() — always use ? placeholders to prevent SQL injection.
8+
9+
Key concepts:
10+
- sqlite3.connect(): opens (or creates) a database file
11+
- cursor.execute(): runs a single SQL statement
12+
- cursor.fetchall(): retrieves all rows from the last query
13+
- conn.commit(): saves changes to disk (INSERT/UPDATE/DELETE need this)
14+
- parameterized queries: use ? placeholders, pass values as a tuple
15+
"""
16+
17+
import os
18+
import sqlite3
19+
20+
21+
# ── Database setup ────────────────────────────────────────────────────
22+
#
23+
# We store the database file in a data/ subdirectory to keep things tidy.
24+
# os.makedirs with exist_ok=True creates the directory if it doesn't exist.
25+
26+
DB_DIR = os.path.join(os.path.dirname(__file__), "data")
27+
DB_PATH = os.path.join(DB_DIR, "books.db")
28+
29+
30+
def create_connection():
31+
"""Open a connection to the SQLite database file.
32+
33+
sqlite3.connect() creates the file if it doesn't exist.
34+
Setting row_factory to sqlite3.Row lets us access columns by name.
35+
"""
36+
os.makedirs(DB_DIR, exist_ok=True)
37+
conn = sqlite3.connect(DB_PATH)
38+
# This makes rows behave like dictionaries — row["title"] works.
39+
conn.row_factory = sqlite3.Row
40+
return conn
41+
42+
43+
# ── Create the table ──────────────────────────────────────────────────
44+
#
45+
# CREATE TABLE IF NOT EXISTS is safe to run multiple times.
46+
# Each column has a type: INTEGER, TEXT, etc. SQLite is flexible about
47+
# types, but declaring them is good practice.
48+
49+
def create_table(conn):
50+
"""Create the books table if it doesn't already exist."""
51+
conn.execute("""
52+
CREATE TABLE IF NOT EXISTS books (
53+
id INTEGER PRIMARY KEY AUTOINCREMENT,
54+
title TEXT NOT NULL,
55+
author TEXT NOT NULL,
56+
year INTEGER NOT NULL
57+
)
58+
""")
59+
conn.commit()
60+
print("Table 'books' created.")
61+
62+
63+
# ── Insert sample data ───────────────────────────────────────────────
64+
#
65+
# executemany() runs the same INSERT for each tuple in the list.
66+
# The ? placeholders get replaced with the tuple values — this is a
67+
# parameterized query. SQLite handles escaping, so special characters
68+
# in the data cannot break the SQL.
69+
70+
SAMPLE_BOOKS = [
71+
("The Pragmatic Programmer", "David Thomas", 1999),
72+
("Clean Code", "Robert C. Martin", 2008),
73+
("Fluent Python", "Luciano Ramalho", 2015),
74+
("Python Crash Course", "Eric Matthes", 2015),
75+
("Design Patterns", "Gang of Four", 1994),
76+
("Refactoring", "Martin Fowler", 1999),
77+
]
78+
79+
80+
def insert_sample_books(conn):
81+
"""Insert sample books into the database."""
82+
conn.executemany(
83+
"INSERT INTO books (title, author, year) VALUES (?, ?, ?)",
84+
SAMPLE_BOOKS,
85+
)
86+
conn.commit()
87+
print(f"Inserted {len(SAMPLE_BOOKS)} books.")
88+
89+
90+
# ── Display helpers ───────────────────────────────────────────────────
91+
92+
def print_books(rows):
93+
"""Print a list of book rows in a readable format."""
94+
for row in rows:
95+
print(f" {row['id']:>2} | {row['title']:<28} | {row['author']:<18} | {row['year']}")
96+
97+
98+
# ── Query functions ───────────────────────────────────────────────────
99+
#
100+
# Each function demonstrates a different kind of SELECT query.
101+
# Notice that every query uses ? placeholders for user-provided values.
102+
103+
def get_all_books(conn):
104+
"""Fetch every book in the table, ordered by ID."""
105+
cursor = conn.execute("SELECT * FROM books ORDER BY id")
106+
return cursor.fetchall()
107+
108+
109+
def get_books_by_year(conn, year):
110+
"""Fetch books published in a specific year.
111+
112+
The (year,) is a one-element tuple. The trailing comma is required —
113+
without it, Python treats the parentheses as grouping, not a tuple.
114+
"""
115+
cursor = conn.execute(
116+
"SELECT * FROM books WHERE year = ?",
117+
(year,), # <-- parameterized: safe from injection
118+
)
119+
return cursor.fetchall()
120+
121+
122+
def search_books_by_title(conn, search_term):
123+
"""Search for books whose title contains the search term.
124+
125+
The LIKE operator with % wildcards does partial matching.
126+
We wrap the search term in % on both sides to match anywhere in the title.
127+
"""
128+
cursor = conn.execute(
129+
"SELECT * FROM books WHERE title LIKE ?",
130+
(f"%{search_term}%",),
131+
)
132+
return cursor.fetchall()
133+
134+
135+
def get_books_by_author(conn, author):
136+
"""Fetch books by a specific author.
137+
138+
This is the simplest parameterized query — exact match on one column.
139+
"""
140+
cursor = conn.execute(
141+
"SELECT * FROM books WHERE author = ?",
142+
(author,),
143+
)
144+
return cursor.fetchall()
145+
146+
147+
# ── SQL injection demo ───────────────────────────────────────────────
148+
#
149+
# This shows what WOULD happen if you used string formatting instead of
150+
# parameterized queries. With parameterized queries, the dangerous input
151+
# is treated as a plain string value, not as SQL code.
152+
153+
def demo_safe_query(conn, dangerous_input):
154+
"""Show that parameterized queries handle dangerous input safely."""
155+
print(f"Searching for: {dangerous_input}")
156+
cursor = conn.execute(
157+
"SELECT * FROM books WHERE author = ?",
158+
(dangerous_input,),
159+
)
160+
rows = cursor.fetchall()
161+
if rows:
162+
print_books(rows)
163+
else:
164+
print("No books found (and the table still exists!).")
165+
166+
167+
# ── Main ──────────────────────────────────────────────────────────────
168+
169+
def main():
170+
# Remove old database so we start fresh each run.
171+
if os.path.exists(DB_PATH):
172+
os.remove(DB_PATH)
173+
174+
conn = create_connection()
175+
176+
print("--- Creating database and table ---")
177+
create_table(conn)
178+
179+
print("\n--- Inserting sample books ---")
180+
insert_sample_books(conn)
181+
182+
print("\n--- All books ---")
183+
print_books(get_all_books(conn))
184+
185+
print("\n--- Books by year 2015 ---")
186+
print_books(get_books_by_year(conn, 2015))
187+
188+
print("\n--- Books with 'Python' in the title ---")
189+
print_books(search_books_by_title(conn, "Python"))
190+
191+
print("\n--- Parameterized query demo ---")
192+
author = "Robert C. Martin"
193+
print(f"Searching for author: {author}")
194+
print_books(get_books_by_author(conn, author))
195+
196+
print("\n--- Dangerous input safely handled ---")
197+
demo_safe_query(conn, "'; DROP TABLE books; --")
198+
199+
# Always close the connection when done.
200+
conn.close()
201+
202+
203+
if __name__ == "__main__":
204+
main()

0 commit comments

Comments
 (0)