Week 11 of 16

Build: The Database Class

Create a proper SQLite storage layer that replaces your JSON-based approach.

Day 53 75 minutes Build

Day 53 of 80

What You're Building

You'll create database.py — a class that wraps all SQLite operations for the Prompt Vault. This is a "data access layer" — it's the one place your app talks to the database. All the SQL lives here; the rest of your code never writes SQL directly.

Why a class? Because a database connection needs to be opened, used, and closed properly. A class bundles that lifecycle together. When you create a Database object, it opens a connection. When you're done, you close it.

Create database.py

In your prompt-vault/ folder, create a new file called database.py:

database.py Python
import sqlite3
from pathlib import Path
from logger import get_logger

logger = get_logger(__name__)


class Database:
    """Manages the SQLite database for the Prompt Vault."""

    def __init__(self, db_path="prompt_vault.db"):
        self.db_path = Path(db_path)
        self.conn = sqlite3.connect(self.db_path)
        # Row factory lets you access columns by name (row['platform'])
        # instead of by index (row[1]).
        self.conn.row_factory = sqlite3.Row
        self._create_tables()
        logger.info(f"Database opened: {self.db_path}")

    def _create_tables(self):
        """Create the prompts table if it doesn't exist."""
        self.conn.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
            )
        """)
        self.conn.commit()

    def add_prompt(self, platform, shot, prompt_text):
        """Insert a new prompt. Returns the new row's ID."""
        cursor = self.conn.execute(
            "INSERT INTO prompts (platform, shot, prompt) VALUES (?, ?, ?)",
            (platform, shot, prompt_text)
        )
        self.conn.commit()
        logger.info(f"Inserted prompt #{cursor.lastrowid}: [{platform}] {shot}")
        return cursor.lastrowid

    def get_all(self):
        """Return all prompts, newest first."""
        cursor = self.conn.execute(
            "SELECT * FROM prompts ORDER BY created_at DESC"
        )
        return cursor.fetchall()

    def get_by_platform(self, platform):
        """Return prompts for a specific platform."""
        cursor = self.conn.execute(
            "SELECT * FROM prompts WHERE platform = ? ORDER BY created_at DESC",
            (platform,)
        )
        return cursor.fetchall()

    def search(self, query):
        """Search prompts by keyword in shot or prompt text."""
        pattern = f"%{query}%"
        cursor = self.conn.execute(
            "SELECT * FROM prompts WHERE shot LIKE ? OR prompt LIKE ?",
            (pattern, pattern)
        )
        return cursor.fetchall()

    def delete(self, prompt_id):
        """Delete a prompt by ID. Returns True if something was deleted."""
        cursor = self.conn.execute(
            "DELETE FROM prompts WHERE id = ?",
            (prompt_id,)
        )
        self.conn.commit()
        deleted = cursor.rowcount > 0
        if deleted:
            logger.info(f"Deleted prompt #{prompt_id}")
        else:
            logger.warning(f"Delete failed: no prompt with id={prompt_id}")
        return deleted

    def count_by_platform(self):
        """Return a dict of platform → count."""
        cursor = self.conn.execute(
            "SELECT platform, COUNT(*) as count FROM prompts GROUP BY platform"
        )
        return {row["platform"]: row["count"] for row in cursor.fetchall()}

    def close(self):
        """Close the database connection."""
        self.conn.close()
        logger.info("Database closed")

cursor.lastrowid gives you the ID the database assigned to the row you just inserted. This is the auto-incremented ID from AUTOINCREMENT. You return it from add_prompt() so the caller knows what ID was created.

cursor.rowcount tells you how many rows were affected by the last DELETE or UPDATE. If it's 0, nothing was deleted (the ID didn't exist). This is how you distinguish "deleted" from "not found."

GROUP BY platform in SQL groups rows with the same platform together. COUNT(*) counts how many rows are in each group. This gives you counts-per-platform in a single database query instead of looping in Python.

Notice there's no add() and no save() separation. Unlike the JSON approach, every database write is immediate. add_prompt() inserts AND commits in one operation — there's no in-memory state to flush.

Test It

Create a quick scratch test to verify everything works:

test_db.py — scratch test (delete after) Python
from database import Database

db = Database("test.db")

# Add some prompts
id1 = db.add_prompt("Kling", "Golf swing", "Slow motion golf swing, golden hour")
id2 = db.add_prompt("Runway", "Ocean aerial", "Aerial tracking over turquoise ocean")
id3 = db.add_prompt("Kling", "Golf aerial", "Drone over golf course, cinematic")

print(f"Added IDs: {id1}, {id2}, {id3}")

# Get all
rows = db.get_all()
print(f"Total: {len(rows)}")
for row in rows:
    print(f"  #{row['id']} [{row['platform']}] {row['shot']}")

# Search
results = db.search("golf")
print(f"\nSearch 'golf': {len(results)} results")

# Platform counts
print(f"Counts: {db.count_by_platform()}")  # {'Kling': 2, 'Runway': 1}

# Delete
db.delete(id2)
print(f"\nAfter delete: {len(db.get_all())} rows")

db.close()

End of Day Checklist

Tomorrow

Day 54 continues the migration — updating vault.py and app.py to use the Database class instead of the JSON-based PromptLibrary.