LEARN TO CODE | SQL | 04 CHANGING DATA AND BUILDING PROJECTS
04 Changing data and building projects: challenges
Demonstrate controlled changes, rollback after failure, safe input handling and a working persistent database project.
START HERE | INDEPENDENT PRACTICE
Use the fresh practice runner for challenges 1–6 and 10–11. Python snippets in 7–8 assume an open connection with the Section 04 schema. Challenge 12 uses the complete 04.4 application.
Every SQL example starts from the original setup, unless the task explicitly combines operations. Predict the target and verify the outcome. Open a hint only when needed and tick the task after checking it. Ticks last while this page remains open.
Use this section’s fresh library practice database. Members stores borrowers, Books stores physical copies , and Loans stores borrowing events. Two copies may share a title but have different IDs and barcodes.
Open the Section 04 runner in VS Code
Create
sql_changes.py
, paste this complete runner and click Run Python File. Change the SQL in changes for the operation and query for the inspection SELECT. Keep setup_sql unchanged. Every run starts fresh, so each example below is independent.
This runner uses SQLite through Python, with explicit SQL transaction control. Outside an explicit BEGIN, individual completed write statements commit automatically. For an atomic multi-statement change, use BEGIN and COMMIT, with error handling that rolls back. The transaction lesson explains this distinction.
import sqlite3
# Explicit SQL transaction control; fresh data on every run.
connection = sqlite3.connect(":memory:", isolation_level=None)
connection.execute("PRAGMA foreign_keys = ON")
setup_sql = """
CREATE TABLE IF NOT EXISTS Members (
member_id INTEGER PRIMARY KEY,
member_name TEXT NOT NULL CHECK (length(trim(member_name)) > 0)
);
CREATE TABLE IF NOT EXISTS Books (
book_id INTEGER PRIMARY KEY,
barcode TEXT NOT NULL UNIQUE CHECK (length(trim(barcode)) > 0),
title TEXT NOT NULL CHECK (length(trim(title)) > 0)
);
CREATE TABLE IF NOT EXISTS Loans (
loan_id INTEGER PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES Members(member_id),
book_id INTEGER NOT NULL REFERENCES Books(book_id),
borrowed_on TEXT NOT NULL,
due_on TEXT NOT NULL,
returned_on TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS one_active_loan_per_book
ON Loans(book_id) WHERE returned_on IS NULL;
INSERT INTO Members VALUES (1, 'Maya'), (2, 'Leo'), (3, 'Amina');
INSERT INTO Books VALUES (10, 'LIB-010', 'The Code Book'),
(11, 'LIB-011', 'The Code Book'), (12, 'LIB-012', 'Python Adventures');
INSERT INTO Loans VALUES
(100, 1, 10, '2026-10-01', '2026-10-15', NULL),
(101, 2, 11, '2026-09-01', '2026-09-15', '2026-09-14');
"""
changes = """
-- Put this exercise's INSERT, UPDATE, DELETE or transaction here.
"""
query = """
SELECT * FROM Loans ORDER BY loan_id;
"""
try:
connection.executescript(setup_sql)
connection.executescript(changes)
cursor = connection.execute(query)
print(" | ".join(column[0] for column in cursor.description))
for row in cursor.fetchall():
print(row)
except sqlite3.Error as error:
if connection.in_transaction:
connection.execute("ROLLBACK")
print("Database error:", error)
finally:
connection.close()
Use executescript only for fixed teaching scripts like these, not text assembled from user input. Do not call executescript in the middle of a manually controlled transaction; the complete application later uses execute for its transaction statements.
REFERENCE DATA
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo |
| 3 | Amina |
| book_id | barcode | title |
|---|---|---|
| 10 | LIB-010 | The Code Book |
| 11 | LIB-011 | The Code Book |
| 12 | LIB-012 | Python Adventures |
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | NULL |
| 101 | 2 | 11 | 2026-09-01 | 2026-09-15 | 2026-09-14 |
CHALLENGE 01 | ADD A BORROWER
Add member 4, Noah, to a fresh practice database. Inspect the result.
Need a hint?
Use an explicit column list.
Reveal a possible solution
INSERT INTO Members (member_id, member_name) VALUES (4, 'Noah');
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo |
| 3 | Amina |
| 4 | Noah |
CHALLENGE 02 | CHANGE ONE RECORD
Rename member 2 to Leo Martin, leaving every other record unchanged.
Need a hint?
Target the stable ID.
Reveal a possible solution
UPDATE Members SET member_name = 'Leo Martin' WHERE member_id = 2;
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo Martin |
| 3 | Amina |
CHALLENGE 03 | RETURN WITHOUT DELETING HISTORY
Record loan 100 as returned on 2026-10-12.
Need a hint?
An event is retained and its status is derived from returned_on.
Reveal a possible solution
UPDATE Loans SET returned_on = '2026-10-12' WHERE loan_id = 100 AND returned_on IS NULL;
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | 2026-10-12 |
| 101 | 2 | 11 | 2026-09-01 | 2026-09-15 | 2026-09-14 |
CHALLENGE 04 | DELETE THE UNREFERENCED COPY
Delete book 12. Then explain why the same command targeting book 10 fails on a fresh setup.
Need a hint?
Check references, not just active status.
Reveal a possible solution
DELETE FROM Books WHERE book_id = 12;
| book_id | barcode | title |
|---|---|---|
| 10 | LIB-010 | The Code Book |
| 11 | LIB-011 | The Code Book |
Book 10 is referenced by loan 100. A returned loan can also keep its parent referenced. Do not delete loan history merely to force the parent deletion.
CHALLENGE 05 | UNDO THE TRIAL
Change every member name to Trial inside a transaction, then undo it. Demonstrate that all original names remain.
Need a hint?
ROLLBACK applies before commit.
Reveal a possible solution
BEGIN;
UPDATE Members SET member_name = 'Trial';
ROLLBACK;
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo |
| 3 | Amina |
The whole-table target here is deliberate and confined to the fresh practice database.
CHALLENGE 06 | ATOMIC HANDOVER
Close loan 100 and create loan 102 for Amina, copy 10, borrowed 2026-10-12 and due 2026-10-26. Commit both together.
Need a hint?
The old active event must be closed before a new active event can pass the index.
Reveal a possible solution
BEGIN IMMEDIATE;
UPDATE Loans SET returned_on = '2026-10-12' WHERE loan_id = 100 AND returned_on IS NULL;
INSERT INTO Loans (loan_id, member_id, book_id, borrowed_on, due_on)
VALUES (102, 3, 10, '2026-10-12', '2026-10-26');
COMMIT;
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | 2026-10-12 |
| 102 | 3 | 10 | 2026-10-12 | 2026-10-26 | NULL |
In application code, inspect the update result and roll back on a failed next statement. Do not assume a statement error undoes the entire transaction automatically.
CHALLENGE 07 | BIND THE NAME
Write a Python lookup for a member name supplied by input, including an apostrophe.
Need a hint?
Use one ? and a one-item tuple.
Reveal a possible solution
wanted = input("Member name: ")
rows = connection.execute(
"SELECT member_id, member_name FROM Members WHERE member_name = ?",
(wanted,),
).fetchall()
Use this snippet with an open library connection. No input text is inserted into SQL syntax.
CHALLENGE 08 | SAFE SORT CHOICES
Offer title or barcode sorting without inserting the supplied choice directly into SQL.
Need a hint?
Map choices to fixed query strings.
Reveal a possible solution
queries = {
'title': "SELECT book_id, title FROM Books ORDER BY title, book_id",
'barcode': "SELECT book_id, title FROM Books ORDER BY barcode, book_id",
}
choice = input("Sort: ").strip()
if choice not in queries:
raise ValueError("Unknown sort option")
rows = connection.execute(queries[choice]).fetchall()
CHALLENGE 09 | PERMISSIONS ARE A SEPARATE TEST
A reporting user sends a perfectly parameterised DELETE. Explain why it should still be rejected and where that rule belongs.
Need a hint?
Query structure and authority answer different questions.
Reveal a possible solution
Binding protects SQL structure, not permission. Enforce the user’s allowed operations in the trusted application and, where supported, database roles. For ordinary SQLite, also protect file access; a hidden delete button does not prevent another write path.
CHALLENGE 10 | OVERDUE AS OF A FIXED DATE
On the original dataset, show active loans overdue as of 2026-10-20 with member and book names.
Need a hint?
Exclude returned loans; due today is not overdue under our rule.
Reveal a possible solution
-- Read-only report; no change statements needed.
| loan_id | member_name | title | due_on |
|---|---|---|---|
| 100 | Maya | The Code Book | 2026-10-15 |
Only loan 100 is active and overdue. In an application, bind the report date instead of constructing SQL from input.
CHALLENGE 11 | PROVE THE ACTIVE-LOAN RULE
Attempt a second active loan for book 10. Explain why it fails, then close loan 100 and repeat successfully.
Need a hint?
Uniqueness applies only where returned_on IS NULL.
Reveal a possible solution
INSERT INTO Loans (loan_id, member_id, book_id, borrowed_on, due_on)
VALUES (102, 3, 10, '2026-10-12', '2026-10-26');
On the original state this fails the partial unique index. On a fresh run, first update loan 100’s returned_on, then this insertion succeeds. Use a transaction for the intended complete handover.
CHALLENGE 12 | CAPSTONE: COMPLETE AND TEST THE LIBRARY
Run the 04.4 application. Demonstrate adding members and copies, issuing, rejecting a duplicate active loan, returning, borrowing again, an overdue report and persistence. Add one retrieval feature of your choice.
Need a hint?
Define the added feature’s output and use parameter binding for supplied values.
Reveal a possible solution
A suitable extension is a member’s loan history filtered by a bound member_id, joining book titles and ordering by borrowing date and loan ID. Record expected and actual results for normal, invalid and boundary inputs. Preserve old events and verify that rejected writes do not affect existing records.
SELECT l.loan_id, b.title, l.borrowed_on, l.returned_on
FROM Loans AS l
INNER JOIN Books AS b ON l.book_id = b.book_id
WHERE l.member_id = ?
ORDER BY l.borrowed_on, l.loan_id;
STRETCH | TEST A FAILED TRANSACTION
Adapt the standalone failure example from 04.2: change a member name and then try inserting a loan referencing a missing member. After the error handler rolls back, inspect both tables. The first change should also be undone.
Explain why the test must not silently commit after catching the error. Keep the transaction boundary around the entire intended operation.
FINISH | YOUR PROJECT EVIDENCE
Keep a short test record covering a valid write, invalid reference, duplicate active loan, date boundary, rollback and reopening the file. State one application-only validation rule and one database-enforced rule.
You have now covered SQL foundations, retrieval, joins, summaries and controlled changes. Choose a small project to extend rather than adding unexplained features. Return to the course index to select your next route.
KEEP LEARNING
Previous page Return to the SQL course
References: Python SQLite parameters and transactions · SQLite transactions · Partial indexes