DELETE and UPDATE
Change and remove rows, through the same plans that find them
Run UPDATE and DELETE as plan nodes over the scan a SELECT would use: find the rows first, then rewrite each row in its cell or remove its cell from the leaf, report how many rows changed, and never let a statement that fails change anything.
- Find, then change: the scan decides which rows; the Update or Delete node above it changes them
- Changing what you are reading: collect the rows before you touch them, or a delete moves cells under its own cursor
- UPDATE in place: a fixed-size row is rewritten in its own cell, and the key never changes
- DELETE removes a cell and nothing else: empty leaves and stale separators are still a valid tree
- Check everything first: a statement either changes every row it selects, or none
Why this stage exists
The table can take rows and give them back, but never let them go or change them. Every real table needs both. This is the last stage of The Core Engine, and it completes the four statements: INSERT, SELECT, UPDATE, DELETE.
It comes last because it needs everything before it. Finding the rows to change is a WHERE. Choosing how to find them is the planner, and doing it fast for id is the index scan. Saying how many rows changed is the affected count. What is new is changing the tree while you read it.
What changes: find, then change
An UPDATE or DELETE is planned like a SELECT with the same WHERE, with one more node on top:
>explain delete from users where id = 3;[PLAN] delete from users where id = 3[PLAN] └── Delete (table=users)[PLAN] └── IndexScan (table=users, key=3)
The scan below finds the rows, exactly the ones a SELECT would print, through the same IndexScan or Filter over SeqScan. The node on top changes them. The success line says how many, and the counter says how many rows the scan examined:
>update users set email = 'bob@home.example' where name = 'bob';Update statement executed successfully (2 rows)[EXEC] rows_examined=4
Do not change what you are reading
The obvious loop is "ask the scan for a row, delete it, ask for the next". It skips rows. Deleting a cell moves the cells after it one place towards the start, so the next row is now where the cursor already was, and the cursor steps past it:
So do it in two passes. First run the scan to the end and keep the key of every row it hands up. Then change the tree, one kept key at a time. The scan never sees a tree that is changing under it, and the count is simply how many keys you kept. Databases call a version of this problem the Halloween problem, after the day it was found: an update that moved rows ahead of its own scan and updated them again.
DELETE removes a cell, and nothing else
A delete is an insert in reverse: find the cell, move the cells after it one place towards the start, take one from the count. Then stop. Do not free a page, merge two leaves, or rewrite a separator, even when a leaf is left empty:
That tree is still correct. A separator only has to divide its children: every key on its left below it, every key on its right at or above it. An empty leaf and a separator nothing on its right equals are both allowed. A search for 7 goes to page 2 and finds nothing, which is the right answer. btree structure shows the empty leaf as LEAF (page=2 cells=0 keys=[]), and pager status counts the same pages as before the delete.
UPDATE rewrites a row in its own cell
Every row is exactly 60 bytes, so a new row fits exactly where the old one was. Read the row, replace the assigned columns, keep the others, and write it back into the same cell. The key does not change, so the row does not move: btree find reports the same page and cell before and after.
That is also why id cannot be assigned. A new key would belong somewhere else in the tree: it would be a delete and an insert, not an update. set id = ... is 00801, even to the value it already has.
All or nothing
A statement is checked completely before the first row changes: the table, each assignment in the order written, then the WHERE. The order and the codes are in the output contract. update users set name = 'x', id = 2 is 00801 and renames nothing: the first assignment is never applied to any row, because the second fails the check before a row is touched. A statement either changes every row it selects, or none.
The algorithm in words
Check the statement
Every error an UPDATE or DELETE can raise is found before the first row changes.
- 1.The table must be users (00401).
- 2.For UPDATE, each assignment in the order written: the column must exist (00402) and must not be id (00801); the value must have the column's type (00403) and fit in 27 bytes (00405).
- 3.Then the WHERE, as for SELECT: its column (00402), its value's type (00403) and range (00404).
- 4.The first check that fails prints its error, and the statement ends having changed nothing.
Find the rows, then change them
The plan is an Update or Delete node over the scan a SELECT with the same WHERE would use: an IndexScan for id, a Filter over a SeqScan for name or email, a SeqScan without WHERE.
- 1.Run the scan to the end, and keep the key of every row it hands up. Count them.
- 2.Only then change the tree, one kept key at a time.
- 3.Print the success line with the count, then the counter of rows the scan examined.
- 4.Invariant: the rows changed are exactly the rows a SELECT with the same WHERE would have printed just before.
Delete a row
Removing a cell is inserting one, backwards.
- 1.Find the leaf and the cell of the key, as btree find does.
- 2.Move every cell after it one cell towards the start of the leaf, and take one from the leaf's count.
- 3.Change nothing else: no page is freed, no leaf is merged or removed, no separator is rewritten, even when the leaf is now empty.
- 4.Invariant: the keys in the leaf are still ascending, and every key the table still has is found where the tree leads.
Update a row
Rows are fixed-size, so the new row fits exactly where the old one was.
- 1.Find the leaf and the cell of the key.
- 2.Read the row, replace the values of the assigned columns, keep the others, and write it back into the same cell.
- 3.Invariant: the key, the cell, the leaf and every other row are unchanged.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, sections 10, 12 and 13.
| Input | Output |
|---|---|
update users set <column> = <value>, ... [where ...]; | Update statement executed successfully (<n> rows), then [EXEC] rows_examined=<m> |
delete from users [where ...]; | Delete statement executed successfully (<n> rows), then [EXEC] rows_examined=<m> |
| either, matching no row | the same line with (0 rows): not an error |
set id = ... | [ERROR:00801] <message> |
| an unknown column, a value of the wrong type, a string over 27 bytes | [ERROR:00402], [ERROR:00403], [ERROR:00405] |
explain update ... / explain delete ... | [PLAN] <statement>, then Update (table=users) or Delete (table=users), then the scan below it |
btree structure with an emptied leaf | [BTREE] LEAF (page=<p> cells=0 keys=[]) |
What you should see when done
$droid --db users.dbWelcome to droid!>insert into users (id, name, email) values (1, 'ada', 'ada@example.com');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (2, 'bob', 'bob@example.com');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (3, 'cy', 'cy@example.com');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (4, 'bob', 'bob@work.example');Insert statement executed successfully (1 rows)>update users set email = 'bob@home.example' where name = 'bob';Update statement executed successfully (2 rows)[EXEC] rows_examined=4>explain delete from users where id = 3;[PLAN] delete from users where id = 3[PLAN] └── Delete (table=users)[PLAN] └── IndexScan (table=users, key=3)>delete from users where id = 3;Delete statement executed successfully (1 rows)[EXEC] rows_examined=1>btree dump[BTREE] Page 0: type=LEAF num_cells=3[BTREE] Cell 0: key=1 (60 bytes)[BTREE] Cell 1: key=2 (60 bytes)[BTREE] Cell 2: key=4 (60 bytes)>update users set id = 9 where id = 1;[ERROR:00801] The primary key cannot be updated>delete from users where id = 3;Delete statement executed successfully (0 rows)[EXEC] rows_examined=0>select * from users;id | name | email1 | ada | ada@example.com2 | bob | bob@home.example4 | bob | bob@home.example(3 rows)[EXEC] rows_examined=3>.exitexiting.. good bye!
The UPDATE on a name examined all 4 rows and changed 2; the DELETE by id examined 1. Key 4 moved into cell 2 when key 3 was deleted. Deleting key 3 a second time found nothing, and said so without an error.
Common bugs in this stage
- Deleting while scanning. Every other row survives a
delete ... where id > 1, or a range delete stops at the end of the first leaf. Collect the keys first, then delete. - A half-done statement. An UPDATE that fails on its second assignment has already applied the first, or a failing statement changes rows before its WHERE is checked. Check everything, then change.
- Moving the wrong cells. The cells to move are those after the deleted one; moving them one cell, 64 bytes. The same overlap rule as inserting applies.
- Forgetting the count. The leaf's count must go down by one per deleted cell, or the old last cell reappears.
- Freeing or merging. A delete that removes an empty leaf, or rewrites a separator, changes pages the tests expect to stay. Remove the cell and stop.
- An update that moves the row. Deleting and re-inserting the row for an UPDATE works, until the key's cell changes or a split happens. Rewrite the row in place.
- Assigning id.
set id = 1on row 1 changes nothing, and is still 00801. - The wrong scan under the node. A DELETE by id that examines every row is a Filter over a SeqScan, not an IndexScan. The node on top changes; the scan below is the SELECT's.
- No match as an error.
(0 rows)is a correct answer for UPDATE and DELETE too.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C and C++:
memmoveagain, this time towards the start of the leaf. A growable array ofuint32_tkeys, or a fixed one of the table's size, is enough to hold the keys between the two passes. - Rust: collecting the keys into a
Vec<u32>before changing the tree is also what lets the borrow checker accept it: the scan's borrow of the pager ends before the first write. - Zig: an
ArrayList(u32)for the collected keys, freed withdeferonce the changes are made.
Checklist
One line per thing the tests check. The tags name the test cases.
- DELETE with WHERE id removes that row and says (1 rows)delete-existing-row
- A DELETE that matches nothing succeeds with (0 rows)delete-non-existing-row
- DELETE without WHERE removes every rowdelete-without-where
- DELETE on a name removes every row with it, through a full scandelete-where-text
- A DELETE of a range removes every row in it, across leavesdelete-range-across-leaves
- The cells after a deleted one move towards the startdelete-shifts-cells
- DELETE frees no page and changes no separator; an emptied tree takes new rows in its old leavesdelete-keeps-the-tree
- A deleted key can be inserted againdelete-then-reinsert
- UPDATE changes only the columns it assignsupdate-existing-row
- An UPDATE that matches nothing succeeds with (0 rows)update-non-existing-row
- UPDATE assigns several columns, and every row without WHEREupdate-several-columns
- UPDATE may change the column its WHERE readsupdate-where-text
- An updated row keeps its key and its cellupdate-keeps-position
- UPDATE cannot assign id, not even to the value it has: 00801update-primary-key
- Every error is found before anything changes, in the contract's orderupdate-delete-errors
- explain shows Update or Delete over the scan a SELECT would useexplain-update-delete
- UPDATE and DELETE are saved, with -c tooupdate-delete-persist
- UPDATE and DELETE work in a tree of depth 4, and leave a valid tree in the fileupdate-delete-deep-tree