Get your hands on sqlite3 before building the real storage layer.
Day 52 of 80
Read the official sqlite3 tutorial. It's short — about 10 minutes. Focus on: opening a connection, executing SQL, committing changes, using row_factory, and closing.
sqlite3 is built into Python — no pip install needed. You already have it.
Open Jupyter (or a scratch .py file) and work through each cell. Don't copy-paste — type it.
import sqlite3
# connect() opens (or creates) a database file.
# Think of it like open() for JSON, but for a database.
# 'test_prompts.db' is just a file on your disk.
conn = sqlite3.connect("test_prompts.db")
# A cursor is how you send SQL commands to the database.
cursor = conn.cursor()
# CREATE TABLE defines the structure — what columns exist and their types.
# IF NOT EXISTS means "only create this if it doesn't already exist."
# INTEGER PRIMARY KEY AUTOINCREMENT gives each row a unique ID automatically.
cursor.execute("""
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
shot TEXT NOT NULL,
prompt TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
# commit() saves the change. Without it, nothing is actually written.
conn.commit()
print("Table created!")
The triple-quoted string lets you write multi-line SQL without awkward concatenation. The SQL goes from """ to """ — Python treats it as one long string.
TEXT NOT NULL means the column stores strings and can't be empty. This is SQL's version of validation — the database itself rejects bad data before Python even sees it.
# The ? placeholders prevent SQL injection — a security vulnerability
# where someone types SQL code into an input field and hacks your database.
# NEVER put user input directly into SQL strings with f-strings.
# Always use ? and pass values as a tuple.
cursor.execute(
"INSERT INTO prompts (platform, shot, prompt) VALUES (?, ?, ?)",
("Kling", "Golf swing follow-through", "Cinematic slow motion golf swing, golden hour...")
)
cursor.execute(
"INSERT INTO prompts (platform, shot, prompt) VALUES (?, ?, ?)",
("Runway", "Ocean aerial", "Aerial tracking shot over turquoise ocean...")
)
conn.commit()
print("Inserted 2 prompts!")
SQL injection is one of the most common security bugs. If you wrote f"INSERT ... VALUES ('{platform}', ...)" and someone entered '); DROP TABLE prompts; -- as a platform name, they could delete your database. The ? pattern makes that impossible.
# fetchall() returns a list of tuples. Each tuple is one row.
cursor.execute("SELECT * FROM prompts")
rows = cursor.fetchall()
for row in rows:
print(row)
# row is a tuple like (1, 'Kling', 'Golf swing...', 'Cinematic...', '2026-...')
# row[0] = id, row[1] = platform, row[2] = shot, etc.
# Row factory makes rows behave like dictionaries.
# This is much nicer to work with than row[1], row[2], etc.
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM prompts WHERE platform = ?", ("Kling",))
rows = cursor.fetchall()
for row in rows:
# sqlite3.Row lets you access columns by name, like a dictionary.
print(f"[{row['platform']}] {row['shot']}")
print(f" {row['prompt'][:60]}...")
print(f" Created: {row['created_at']}\n")
Always set row_factory before creating a cursor. The row factory applies to all cursors created after it's set. Setting it once on the connection is the standard pattern — you'll do this in your Database class tomorrow.
# LIKE is SQL's fuzzy match. % is a wildcard — matches any characters.
# '%golf%' matches anything containing "golf" anywhere in the value.
cursor.execute(
"SELECT * FROM prompts WHERE shot LIKE ? OR prompt LIKE ?",
("%golf%", "%golf%")
)
results = cursor.fetchall()
print(f"Found {len(results)} matches for 'golf'")
for row in results:
print(f" [{row['platform']}] {row['shot']}")
# Always close the connection when you're done.
conn.close()
The % wildcard lets you match anywhere in the string. '%golf%' matches "Golf swing", "slow motion golf", "Golf course aerial" — any value containing "golf." This is what your search feature will use.
Think about how this differs from your JSON approach:
SELECT * FROM prompts WHERE platform = 'Kling' only returns Kling prompts. With JSON, you loaded all prompts then filtered in Python.commit(). No accidental overwrites.cursor is and why you need one? placeholders matter (SQL injection)row_factory = sqlite3.Row and used column namescommit() does and when to call itDays 53–54 build the Database class — a proper SQLite storage layer that replaces your JSON-based PromptLibrary. You'll use everything from today's experiments.