|
| 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