The bakery, stage 2: the data
In short: Stage 2 fills the bakery tables with INSERT, RETURNING and INSERT … SELECT, and records the first order inside a transaction.
Problem
Empty tables
The tables from stage 1 are ready and empty. The bakery has three categories, eight products, five customers and its first orders. In this stage you write them in, and every rule from stage 1 will guard the data.
All the fields start from the database you have made so far: the five tables and the data from the earlier steps of the stage.
The idea
The first row
INSERT INTO table (columns) VALUES (values) writes a row. The column id is not in the list: INTEGER PRIMARY KEY without a value gets the next number on its own.
The columns in brackets matter. INSERT INTO categories VALUES (1, 'Bread') works too, but it depends on the order of the columns in the table and breaks with the first new column.
Try it
The three categories
Write the three categories with one INSERT: Bread, Pastries and Drinks, in this order (bread is 1, pastries 2, drinks 3).
Solution
INSERT INTO categories (name) VALUES
('Bread'),
('Pastries'),
('Drinks');Several rows in VALUES, separated by commas: ('Bread'), ('Pastries'), ('Drinks'). The order of writing gives the numbers. With the drinks before the pastries the drinks become 2, and the products later point to the wrong category.
The idea
What the database wrote
The program often needs the number of the new row, for example to show "The product was added with number 1". RETURNING returns the columns of the written row:
RETURNING also returns the values the database put in on its own: without stock in the list it would return 0 from DEFAULT. The eight products are written with one INSERT:
More precisely, if you are curious
RETURNING exists in SQLite since version 3.35 and in PostgreSQL. MySQL does not have it: there the number is taken with LAST_INSERT_ID(), and in SQLite also with last_insert_rowid().
See it
The rules guard
The rules from stage 1 are now working. A product with price 0:
A product in category 9, which does not exist:
A product without a name:
None of the three goes in. The error comes from the database, so it does not matter who sends the row: the site, an import script from Excel or a colleague in the console.
Try it
The customers
Write the five customers with one INSERT (columns name, email, city): Ivan Kolev, ivan.kolev@example.com, Sofia; Ana Petrova, ana.petrova@example.com, Plovdiv; Boris Nikolov, boris.nikolov@example.com, Sofia; Desislava Marinova, desislava.marinova@example.com, Varna; Kalin Todorov, kalin.todorov@example.com, no city.
Solution
INSERT INTO customers (name, email, city) VALUES
('Ivan Kolev', 'ivan.kolev@example.com', 'Sofia'),
('Ana Petrova', 'ana.petrova@example.com', 'Plovdiv'),
('Boris Nikolov', 'boris.nikolov@example.com', 'Sofia'),
('Desislava Marinova', 'desislava.marinova@example.com', 'Varna'),
('Kalin Todorov', 'kalin.todorov@example.com', NULL);Kalin's city is NULL, without quotes. '' is empty text: a city that is known and is empty, not "we do not know". WHERE city IS NULL would then not find Kalin. The e-mails are written exactly like this, in small letters.
More precisely, if you are curious
UNIQUE compares the text letter by letter: Ivan.Kolev@example.com and ivan.kolev@example.com are two different e-mails for the database. That is why the program turns the e-mail into small letters before writing it, or the table keeps UNIQUE on lower(email) with an index (CREATE UNIQUE INDEX … ON customers (lower(email))).
See it
The same e-mail
Ivan from Plovdiv is another person with the same e-mail, and the database does not let him in. Signing up on the site shows exactly this error as "This e-mail is already registered".
The idea
The order and its rows
An order without status gets 'new' from DEFAULT:
A real order, though, is two kinds of rows: one in orders and several in order_items. If the program crashes between them, an order without a single product is left. That is why the two are written in a transaction: BEGIN, the inserts, COMMIT. Until COMMIT nothing is final.
Try it
The first order
Ivan (customer 1) ordered on 2026-10-01 two country breads (product 1 at 2.40) and three croissants (product 4 at 1.30). The order is delivered. Write it in one transaction: the order (it will be number 1) and its two rows.
Solution
BEGIN;
INSERT INTO orders (customer_id, ordered_at, status) VALUES (1, '2026-10-01', 'delivered');
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1, 1, 2, 2.40),
(1, 4, 3, 1.30);
COMMIT;BEGIN;, INSERT INTO orders (customer_id, ordered_at, status) VALUES (1, '2026-10-01', 'delivered');, then INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (1, 1, 2, 2.40), (1, 4, 3, 1.30); and COMMIT;. A row with order 2 is stopped by the foreign key: there is no such order yet. The croissant at 2.40 is the price of the bread, not its own.
See it
Cancelling
The program has written the order and finds out that something is wrong (the card was declined, a row stopped with an error). Then instead of COMMIT it sends ROLLBACK:
During the transaction there are 2 orders, and after ROLLBACK 1 again: the second has disappeared, as if there had been no INSERT.
More precisely, if you are curious
Here every field stops at the first error, like the console. The program does the same with try: on an exception it sends ROLLBACK. The transaction is not cancelled on its own when one query fails, so ROLLBACK is the program's job.
See it
The other orders
Orders 2 to 6 are written the same way. Here they are, with the number of orders by status at the end:
Order 4 is cancelled, 5 is paid and waits for delivery, and 6 is new. Kalin (customer 5) has not ordered yet.
Exercise
The price from the product
The site does not know the prices by heart. When the customer buys, the price is taken from products at that moment and written into the row. INSERT … SELECT writes the rows that a SELECT returns.
Kalin (customer 5) orders two baguettes (product 3) today. Write the order (it will be number 7, the date is date('now')) and its row, taking unit_price from products with INSERT … SELECT.
Solution
INSERT INTO orders (customer_id, ordered_at) VALUES (5, date('now'));
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT 7, id, 2, price FROM products WHERE id = 3;INSERT INTO order_items (order_id, product_id, quantity, unit_price) SELECT 7, id, 2, price FROM products WHERE id = 3;. The SELECT returns one row: 7, 3, 2 and the price of the baguette. With 1.80 by hand the price is wrong (that is the coffee), and with WHERE id = 2 the row is for the rye bread.
The idea
The full database
The six orders are in the database. How many rows each table has:
The next stage asks questions about this data: the menu, the bill of every order, who buys what.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Constraints: rules for the data · Natural keys and denormalization · ACID: all or nothing · BEGIN, COMMIT, ROLLBACK
- Tasks on this topic: Project: the bakery from scratch