LEARN TO CODE | SQL | 01 RELATIONAL DATABASE FOUNDATIONS
01 | Relational database foundations: challenges
Apply what you have learned about tables, data types, keys and constraints, then design and test a small database of your own.
BEFORE YOU START
These challenges cover lessons 01.1–01.4. Work in your book for the planning tasks and use SQLite for the coding tasks. There are no query joins, aggregation tasks or application projects to master here: focus on a sound structure and its rules.
Use the in-memory Python runner from 01.3 Creating tables and constraints . Put SQL inside its triple-quoted sql string and click Run in VS Code. Each run starts fresh. Its final SELECT prints Students, so adapt that inspection statement when you build a differently named table, as each task explains.
For challenges 1–5, refer to the three-table example below. Challenges 6–7 use an independent Workshops table. Challenges 8–9 use a fresh copy of the clubs example. Challenge 10 is a separate equipment project.
Attempt a task before opening the hint. Tick it only after checking the result and explaining it. Your ticks last while this page remains open.
REFERENCE DATA | THE CLUBS DATABASE
| student_id | first_name | year_level |
|---|---|---|
| 101 | Maya | 10 |
| 102 | Leo | 11 |
| 103 | Maya | 10 |
| club_id | club_name | capacity |
|---|---|---|
| 1 | Robotics | 12 |
| 2 | Game Design | 16 |
| student_id | club_id | role |
|---|---|---|
| 101 | 1 | member |
| 101 | 2 | leader |
| 102 | 1 | member |
Students and Clubs use their ID columns as primary keys. Memberships uses the pair (student_id, club_id) as its composite primary key and has a foreign key to each parent table. References and roles are required.
CHALLENGE 01 | READ THE STRUCTURE
For Students, state the number of records and fields. Identify the value of first_name in student 102’s record. Explain why the two Mayas do not create an identity problem in this design.
Predict the effect of adding student 104 without adding any columns.
Need a hint?
Separate headers, complete rows and individual field values. A key identifies a row even if descriptive values repeat.
Reveal a possible solution
There are three records and three fields. Student 102’s first_name is Leo. The two Mayas have different primary keys, 101 and 103. After adding student 104, there are four records and still three fields.
CHALLENGE 02 | CHOOSE TYPES AND MISSING-VALUE RULES
Choose suitable SQLite storage choices for: a first name, a year level, an asset tag such as AIR-010, a phone number beginning with zero and a measured mass of 2.75 kg.
A club’s room has not yet been assigned. Explain how NULL differs from an empty string and the number zero.
Need a hint?
Choose based on meaning, not whether digits are present. Ordinary SQLite declared types are not complete validation rules.
Reveal a possible solution
Names, asset tags and phone numbers suit TEXT. Year level suits INTEGER with an appropriate whole-number and range rule. A measured mass can use REAL. NULL can represent an unassigned room; an empty string is an actual text value and zero is a known numeric value. Decide whether the room is optional in the schema.
CHALLENGE 03 | FOLLOW THE KEYS
Identify the clubs joined by student 101 and the students recorded in Robotics. State student 101’s role in Game Design. Explain why first_name should not replace student_id in Memberships.
Need a hint?
Follow the membership ID values to matching parent records. Do not match on a name alone.
Reveal a possible solution
Student 101 joins Robotics and Game Design and leads Game Design. Students 101 and 102 are recorded in Robotics. Names can repeat or change, so a stable unique identifier avoids ambiguity.
CHALLENGE 04 | REPAIR A MANY-TO-MANY DESIGN
A proposed Students table has columns student_id, first_name, club1, club2 and club3. Explain two problems. Draw a replacement using Students, Clubs and Memberships, labelling primary and foreign keys.
State which table should store a student’s role in a particular club.
Need a hint?
One student can join many clubs and one club can contain many students. The relationship can carry its own facts.
Reveal a possible solution
The fixed columns impose an arbitrary club limit and make membership operations awkward. Store club details once in Clubs and one student–club pair per row in Memberships. Students.student_id and Clubs.club_id are primary keys. Memberships has required foreign keys to both and uses the pair as a primary key. Role belongs to Memberships because it can vary between the same student’s clubs.
CHALLENGE 05 | PREDICT THE RULE
Decide whether each proposed membership is accepted after the reference data, and give the reason: (103, 2, member), (101, 1, leader), (999, 1, member), and (103, 2, captain).
Treat each proposal independently against the original data, with foreign keys enabled. Do not let an earlier answer change a later one.
Need a hint?
Check required parent records, the uniqueness of the pair and the allowed role.
Reveal a possible solution
(103, 2, member) is accepted. (101, 1, leader) is rejected because the pair already exists, even though the role differs. (999, 1, member) is rejected because student 999 is absent. (103, 2, captain) is rejected by the role rule; this pair is unused in the original data, so it isolates the invalid role.
CHALLENGE 06 | DEBUG CREATE TABLE
This independent Workshops definition has syntax problems and incomplete rules. Correct the commas, then require a name that is not NULL or blank and a whole-number places value of at least 1.
CREATE TABLE Workshops (
workshop_id INTEGER PRIMARY KEY
workshop_name TEXT NOT NULL,
places INTEGER CHECK (places >= 1),
);
Replace the runner’s SQL string with your solution. Change the final query to
SELECT * FROM Workshops
. An empty result is expected until you insert a row.
Need a hint?
A comma separates definitions, but there is no final comma. NOT NULL and a value check do different jobs.
Reveal a possible solution
CREATE TABLE Workshops (
workshop_id INTEGER PRIMARY KEY,
workshop_name TEXT NOT NULL
CHECK (length(trim(workshop_name)) > 0),
places INTEGER NOT NULL
CHECK (typeof(places) = 'integer' AND places >= 1)
);
The missing comma after workshop_id and the extra final comma are repaired. typeof checks the stored representation after SQLite affinity; it does not forbid every numeric text input from being converted into an integer.
CHALLENGE 07 | PROVE YOUR CONSTRAINTS WORK
Using your corrected Workshops definition, add one test INSERT at a time. Test a valid row, an empty name, a NULL name, places = 0, places = 3.5 and a repeated primary key.
Record the expected result, actual result and relevant rule. For the duplicate-key test, insert the valid row first, then a second row with the same ID in that run.
Need a hint?
Start a fresh in-memory database for each isolated test. An error stops the runner before its normal output loop.
Reveal a possible solution
| Test | Expected result |
|---|---|
| (1, 'Robotics', 12) | Accepted |
| (1, '', 12) | Rejected: name CHECK |
| (1, NULL, 12) | Rejected: NOT NULL |
| (1, 'Robotics', 0) | Rejected: places CHECK |
| (1, 'Robotics', 3.5) | Rejected: whole-number CHECK |
| A second row with ID 1 | Rejected: PRIMARY KEY |
INSERT INTO Workshops (workshop_id, workshop_name, places)
VALUES (1, 'Robotics', 12);
For an ordinary isolated test, replace the values and Run again. For the duplicate test, retain this statement and append another using ID 1.
CHALLENGE 08 | ADD A DEFAULT WITHOUT LOSING THE RULE
In a fresh copy of the clubs schema from 01.4, add an equipment_count column to Clubs. It is required, defaults to zero and accepts non-negative whole numbers.
Update any club INSERT statements so existing values still map to the intended columns. Demonstrate omission, 5, -1, NULL and 2.5.
Need a hint?
Use an explicit INSERT column list. DEFAULT applies when the column is omitted, not when NULL is explicitly supplied.
Reveal a possible solution
equipment_count INTEGER NOT NULL DEFAULT 0
CHECK (typeof(equipment_count) = 'integer' AND equipment_count >= 0)
Add this as a column definition with the appropriate separating comma. The original 01.4 club inserts name their columns, so they omit the new column and receive zero.
INSERT INTO Clubs (club_id, club_name, capacity)
VALUES (3, 'Art', 10);
INSERT INTO Clubs (club_id, club_name, capacity, equipment_count)
VALUES (4, 'Film', 8, 5);
Omission gives 0, and 5 succeeds. -1 and 2.5 fail the check; NULL fails NOT NULL. Test each invalid case separately.
CHALLENGE 09 | EXPLAIN THE LIMITS
A club has capacity 1, but the database accepts memberships for two different existing students. Has SQLite ignored the CHECK on capacity?
Also explain why a TEXT joining_date does not by itself guarantee a real calendar date, and why a table named Memberships does not automatically create foreign keys.
Need a hint?
Distinguish a value rule from a rule comparing several records. Names and declared types do not supply every intended behaviour.
Reveal a possible solution
The capacity CHECK only requires a positive whole number. It does not count membership rows. Enforcing the cross-row limit needs further application logic or a carefully designed database mechanism. TEXT alone does not validate a date. Foreign keys must be declared and enforced; SQLite enforcement must be enabled per connection before the transaction begins.
CHALLENGE 10 | CAPSTONE: EQUIPMENT LOANS
Design a small Maker Space loans database. Each physical item has a unique asset tag, even when several items share a description. A borrower can make many loans, and the same borrower can borrow the same item again on another occasion.
- Store borrowers, physical equipment and individual loan events.
- Give each loan its own identifier and required references.
- Store a required loan-date text value using a documented YYYY-MM-DD convention.
- Store returned as a required 0/1 integer flag, defaulting to 0.
- Insert two borrowers, two same-description items with different tags and three loans, including repeat borrowing.
Submit a diagram, data dictionary, CREATE TABLE script, sample data and at least five tests. State explicitly which rules your design does not enforce.
Use a fresh runner. Its final query should inspect Loans instead of Students. For repeat borrowing, represent the earlier loan as returned. Preventing overlapping active loans is a stretch requirement, not enforced by this starter design.
Need a hint?
A borrower–equipment pair cannot be the only primary key if the same pair must appear in separate historical loan events.
Reveal a possible solution
PRAGMA foreign_keys = ON;
CREATE TABLE Borrowers (
borrower_id INTEGER PRIMARY KEY,
borrower_name TEXT NOT NULL
CHECK (length(trim(borrower_name)) > 0)
);
CREATE TABLE Equipment (
equipment_id INTEGER PRIMARY KEY,
asset_tag TEXT NOT NULL UNIQUE
CHECK (length(trim(asset_tag)) > 0),
item_name TEXT NOT NULL
CHECK (length(trim(item_name)) > 0)
);
CREATE TABLE Loans (
loan_id INTEGER PRIMARY KEY,
borrower_id INTEGER NOT NULL,
equipment_id INTEGER NOT NULL,
loan_date TEXT NOT NULL,
returned INTEGER NOT NULL DEFAULT 0
CHECK (typeof(returned) = 'integer' AND returned IN (0, 1)),
FOREIGN KEY (borrower_id) REFERENCES Borrowers(borrower_id),
FOREIGN KEY (equipment_id) REFERENCES Equipment(equipment_id)
);
INSERT INTO Borrowers (borrower_id, borrower_name)
VALUES (1, 'Maya'), (2, 'Leo');
INSERT INTO Equipment (equipment_id, asset_tag, item_name)
VALUES (10, 'AIR-010', 'Microcontroller kit'),
(11, 'AIR-011', 'Microcontroller kit');
INSERT INTO Loans
(loan_id, borrower_id, equipment_id, loan_date, returned)
VALUES (100, 1, 10, '2026-10-01', 1),
(101, 1, 10, '2026-10-08', 0);
INSERT INTO Loans (loan_id, borrower_id, equipment_id, loan_date)
VALUES (102, 2, 11, '2026-10-08');
Borrowers and Equipment each have a one-to-many relationship to Loans. loan_id identifies an event, allowing repeated borrower–item pairs. Unique asset tags distinguish the two physical kits. Omitting returned for loan 102 produces 0.
The schema does not validate an actual calendar date or stop overlapping active loans. It also does not verify that a typed borrower name is genuine. Do not claim those rules have been implemented merely because the relevant fields exist.
| Independent test after fresh setup | Expected |
|---|---|
| INSERT INTO Loans VALUES (103, 999, 11, '2026-10-09', 0); | Foreign key failure |
| INSERT INTO Equipment VALUES (12, 'AIR-010', 'Camera'); | Unique tag failure |
| INSERT INTO Loans VALUES (103, 2, 11, '2026-10-09', 2); | Flag CHECK failure |
| INSERT INTO Loans VALUES (103, 2, 11, NULL, 0); | NOT NULL failure |
| INSERT INTO Loans VALUES (100, 2, 11, '2026-10-09', 0); | Duplicate loan key failure |
STRETCH | CHANGE THE REQUIREMENT
Plan, without necessarily coding, how you would record annual club memberships rather than only current memberships. State why the existing student–club pair key blocks repeated membership in a later year, then propose a suitable uniqueness rule.
Reveal one design direction
Introduce an academic_year field and require each (student_id, club_id, academic_year) combination to be unique. This could be the composite primary key with all parts required, or a separate membership ID plus a UNIQUE rule on the required triple. Define exactly what an academic year means and how it is represented.
For equipment loans, plan how to reject a second active loan for the same physical item while preserving past events. Research and implement this later rather than adding an arbitrary UNIQUE(equipment_id), which would prevent all repeat loans.
FINISH | CHECK YOUR EVIDENCE
You should have more than a script that runs. Check that you can explain one row’s meaning, justify types and keys, trace references, and demonstrate that each implemented rule accepts and rejects suitable examples.
In your book, record one design improvement and one rule not yet enforced. If you needed a solution, close it and rebuild that part independently.