droid
DROP TABLE and TRUNCATE
Memory and Space · Stage 5 of 8

DROP TABLE and TRUNCATE

Throwing a table away is a walk over its tree that hands every page to the free list

Add DROP TABLE [IF EXISTS] and TRUNCATE [TABLE]. DROP frees every page of the table's tree, children before their parent, and removes its catalog row. TRUNCATE frees every page but the root and leaves the root an empty leaf, so the table stays as it was, with no rows.

Not run yet · 11 tests
Concepts in this stage
  • A table is a catalog row plus a tree of pages: dropping it means giving back both
  • Freeing a page overwrites it, so a node is freed only after everything it points at
  • Post-order: the order that never needs a page that has already been freed
  • TRUNCATE keeps the root on the same page, so the catalog row does not change
  • IF EXISTS turns a missing table from an error into a statement that does nothing
Where this stage sits in the engine
Talking to the user
REPL
Sessions
Printer
Server
Wire protocol
Understanding SQL
Lexer
Parser
Validator
Answering a query
Planner
Executor
Keeping changes safe
Transactions
Write-ahead log
Undo log
Lock manager
Read views
Storing rows
Row formatSlotted page
B+Tree
Reaching the disk
Pager
Database file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

You can create a table, but not get rid of one. Once made, a table's catalog row and every page of its tree are in the file forever, however useless they become. DELETE does not help: it removes rows, but the pages that held them stay part of the tree.

The free list from the free-list stage is what makes this stage possible. A table is two things, a row in the catalog and a tree of pages, and dropping it gives both back: the row is deleted from the catalog, and the pages go on the free list, where the next table, or the next leaf split of any table, finds them. TRUNCATE is the same walk with one page kept, so the table survives with nothing in it.

What changes: four keywords and two statements

DROP TABLE [IF EXISTS] t;
TRUNCATE [TABLE] t;

drop, truncate, if and exists join the keywords, so tokenize prints them as KEYWORD_DROP and so on, in the case they were typed. ast prints:

Statementast
drop table t;Statement: DROP TABLE, Table: t
drop table if exists t;Statement: DROP TABLE IF EXISTS, Table: t
truncate table t;, truncate t;Statement: TRUNCATE TABLE, Table: t

What changes: a walk that frees a tree

To free a tree you have to find all of its pages, and the only way to find them is to read the tree: an internal node lists its children, and nothing else does. That is where the order matters. Freeing a page turns it into a free page, type 4 and the next free page at bytes 10 to 13, and wipes everything else, including the list of children. Free an internal node first and its children are lost: you can no longer read where they are.

So each node is freed after everything under it. Read the node; if it is internal, walk each child, left to right, all the way down; only then free the node. This is a post-order walk, and it has a simple invariant: no page is ever read after it has been freed.

Take users with thirty 502-byte rows, a tree of three levels:

users with 30 rows, on pages 2 to 11
internal · page 2
[13]
internal · page 8
[5,9]
leaf · page 3
[1,2,3,4]
next → page 4
leaf · page 4
[5,6,7,8]
next → page 5
leaf · page 5
[9,10,11,12]
next → page 6
internal · page 9
[17,21,25]
leaf · page 6
[13,14,15,16]
next → page 7
leaf · page 7
[17,18,19,20]
next → page 10
leaf · page 10
[21,22,23,24]
next → page 11
leaf · page 11
[25,26,27,28,29,30]
next → none
The page numbers are the order the pages were allocated in, as leaves and internal nodes split. They are not the order the walk frees them in.

The walk frees the pages in this order, each one going on the head of the free list:

StepFreedWhy now
1–33, 4, 5the leaves under page 8, left to right
48all its children are free
5–86, 7, 10, 11the leaves under page 9
99all its children are free
102the root, last

The last page freed is the head, so db freelist shows the walk backwards: chain=[2,9,11,10,7,6,8,5,4,3]. The order is part of the contract, so every implementation prints the same chain.

DROP TABLE frees the whole tree, root included, and then deletes the table's row from the catalog. Nothing else names the table's pages, so after the DROP they belong to the free list alone. The name can be used again at once, and the next CREATE TABLE gets an id one more than the largest left in the catalog.

TRUNCATE does the same walk but keeps the root. Every page under it is freed, and the root is written again as an empty leaf on the same page. Because the root does not move, the catalog row, which says where it is, does not change: the table has the same columns, the same id and the same root, and no rows.

Before: users with 8 rows
internal · page 2
[5]
leaf · page 3
[1,2,3,4]
next → page 4
leaf · page 4
[5,6,7,8]
next → none
After TRUNCATE users: pages 3 and 4 are on the free list
leaf · page 2 · changed
[]
next → none
The root stays on page 2, now a leaf with no cells. TRUNCATE freed 3, then 4, so db freelist prints chain=[4,3]; the next split takes page 4 first.

A table that does not exist is 00401 for both statements, and changes nothing. IF EXISTS turns that into success: DROP TABLE IF EXISTS nope; prints the usual line and does nothing, which is what a setup script wants when it does not know whether the table is there yet.

Real databases do more on a DROP: PostgreSQL deletes the table's files and every index and constraint that depends on it, and refuses (without CASCADE) when a view or a foreign key still uses it. Here a table has nothing that depends on it yet; foreign keys arrive with Joins and Relations.

The algorithm in words

1

Parse DROP and TRUNCATE

Four new keywords and two new statements.

  1. 1.drop, truncate, if and exists become keywords, in any case, like create and table.
  2. 2.DROP must be followed by TABLE, then optionally IF EXISTS, then the table name. Anything else is 00302.
  3. 3.TRUNCATE is followed by an optional TABLE, then the table name.
  4. 4.ast prints the statement kind, with IF EXISTS when it was given, and the table.
2

Free a tree

A walk that visits every page of the tree once and frees it after its children.

  1. 1.Read the node. If it is internal, walk each child in key order, the right child last, before doing anything to the node.
  2. 2.Then free the node: it becomes a free page at the head of the list, as in the free-list stage.
  3. 3.Invariant: no page is read after it has been freed, and every page of the tree is freed exactly once.
3

DROP TABLE

Every page of the tree, then the catalog row.

  1. 1.Look the table up. A table that does not exist is 00401 and nothing changes, unless IF EXISTS was given: then print the success line and change nothing.
  2. 2.Free the whole tree, starting at the root the catalog names.
  3. 3.Delete the table's row from the catalog. The name is free again, and the next CREATE TABLE gets an id one more than the largest left.
  4. 4.Print the success line, with no count.
4

TRUNCATE

Every page but the root, then an empty root.

  1. 1.Look the table up; a missing table is 00401.
  2. 2.Walk the tree as for DROP, but stop before the root: free every child's pages and keep the root itself.
  3. 3.Write the root again as an empty leaf on the same page, a root with no next link. The catalog row, which names that page, stays as it is.
  4. 4.Print the success line, with no count.

Output contract for this stage

The full definition is on the output contract page, section 27.

InputOutput
DROP TABLE t;Drop table statement executed successfully
DROP TABLE IF EXISTS t;the same line, also when t does not exist
TRUNCATE [TABLE] t;Truncate table statement executed successfully
DROP or TRUNCATE of a table that does not exist[ERROR:00401] <message>
explain drop table t;[PLAN] drop table t, [PLAN] └── DropTable (table=t)
explain truncate t;[PLAN] truncate t, [PLAN] └── TruncateTable (table=t)

Neither line has a count. Pages are freed in post-order: each child's pages, children in key order, then the node; TRUNCATE stops before the root. explain checks the table as running would, so a missing table is 00401 and no plan, except under IF EXISTS, where the plan is printed. It changes nothing.

What you should see when done

On a file with users, eight 502-byte rows:

droid session
Welcome to droid!
>create table items (id int, label varchar(40));
Create table statement executed successfully
>btree structure users
[BTREE] Tree depth: 2
[BTREE] INTERNAL (page=2 keys=[5])
[BTREE] LEAF (page=3 cells=4 keys=[1,2,3,4])
[BTREE] LEAF (page=4 cells=4 keys=[5,6,7,8])
>drop table users;
Drop table statement executed successfully
>db freelist
[DB] Free list: pages=3 chain=[2,4,3]
>select * from users;
[ERROR:00401] Unknown table: users
>create table users (id int, name varchar(1000), email varchar(1000));
Create table statement executed successfully
>catalog list
[CATALOG] Tables: 2
[CATALOG] items root=5 (id INT, label VARCHAR(40))
[CATALOG] users root=2 (id INT, name VARCHAR(1000), email VARCHAR(1000))
>db freelist
[DB] Free list: pages=2 chain=[4,3]
>drop table if exists nope;
Drop table statement executed successfully
>truncate nope;
[ERROR:00401] Unknown table: nope
>explain truncate items;
[PLAN] truncate items
[PLAN] └── TruncateTable (table=items)
>.exit
exiting.. good bye!

Things to notice. The DROP freed the root last, so it heads the list, and the new users took it for its root: the file did not grow. The catalog now lists users after items, because the new users has the larger id. The other two free pages wait for the next split.

Common bugs in this stage

  • Freeing a node before its children. Freeing wipes the page, so the children's page numbers are gone, and the walk either stops early or follows zeros. Read a node's children before freeing it, and free it last.
  • Following leaf links instead of the tree. The leaf chain reaches every leaf but no internal node, so the internal pages are never freed. Walk from the root.
  • Forgetting the catalog row. The pages are free but the table is still listed, with a root that is now a free page; the next SELECT on it is 00703 or worse.
  • Moving the root on TRUNCATE. Freeing the root too and allocating a new one works only if the catalog row is updated, and the contract says it is not: the root stays on its page.
  • A stale next link on the truncated root. The root becomes a leaf with no cells and no next link. If it keeps the bytes of an old leaf header, a scan follows a link into a free page.
  • Caching a dropped table. Anything kept in memory about the table, its schema or its root, must go with the DROP, or a statement on it still runs.

Language notes

The page above is the same for every language. These are the only language-specific hints.

  • C: a recursive walk is fine, as the tree is a few levels deep. Copy a node's child page numbers into a small array before recursing, so nothing depends on the page staying in the pool.
  • C++: collect the children into a std::vector<uint32_t> first, then recurse; the page buffer may be evicted while you walk below it.
  • Rust: read the children into a Vec<u32> and drop the borrow of the page before recursing, which the borrow checker will insist on anyway.
  • Zig: recursion with a fixed-size array of children works; pass the allocator if you prefer an ArrayList(u32).

Checklist

One line per thing the tests check. The tags name the test cases.

  • DROP TABLE removes the table from the catalog; every statement on it is 00401
    drop-removes-from-catalog
  • Dropping one table leaves the others and their rows alone
    drop-one-leaves-other
  • DROP TABLE frees every page of the tree, in post-order
    drop-frees-every-page
  • A table created after a DROP reuses the freed pages; the file does not grow
    drop-then-create-reuses
  • DROP and TRUNCATE of a missing table are 00401 and change nothing
    drop-unknown-table
  • DROP TABLE IF EXISTS of a missing table succeeds and changes nothing
    drop-if-exists
  • TRUNCATE empties a table and keeps its columns and its catalog row
    truncate-keeps-schema
  • TRUNCATE frees every page but the root, which becomes an empty leaf
    truncate-frees-all-but-root
  • drop, truncate, if and exists are keywords
    tokenize-drop
  • ast prints DROP TABLE, DROP TABLE IF EXISTS and TRUNCATE TABLE
    ast-drop
  • explain prints DropTable or TruncateTable and changes nothing
    explain-drop