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

Expected result after this independent example
member_id member_name
1 Maya
2 Leo
3 Amina
Expected result after this independent example
book_id barcode title
10 LIB-010 The Code Book
11 LIB-011 The Code Book
12 LIB-012 Python Adventures
Expected result after this independent example
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');
Expected result after this independent example
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;
Expected result after this independent example
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;
Expected result after this independent example
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;
Expected result after this independent example
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;
Expected result after this independent example
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;
Expected result after this independent example
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.
Expected result after this independent example
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