droid
ALTER TABLE ADD COLUMN
Querying · Stage 11 of 11

ALTER TABLE ADD COLUMN

Change a table's schema without touching a single row

Parse ALTER TABLE ADD COLUMN with NOT NULL and DEFAULT, change only the catalog, and read every row written before the change as if it had the new column, filled with its default.

Not run yet · 13 tests
Concepts in this stage
  • Schema change without a rewrite: the catalog changes, the rows do not
  • Every row carries its own column count, so a reader knows which columns it lacks
  • A missing column reads as its default, NULL when it has none
  • DEFAULT is one idea in three places: ADD COLUMN, CREATE TABLE and the columns an INSERT leaves out
  • NOT NULL without a DEFAULT cannot be added to rows that already exist
  • DDL is transactional: ROLLBACK takes an ALTER back
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
TransactionsWrite-ahead log
Undo log
Lock manager
Read views
Storing rows
Row format
Slotted page
B+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

Tables change. A product gains a discount column; a user table gains last_login. The obvious way to add a column is to read every row, add the new value, and write it back. On a table of a hundred million rows that takes hours, holds a transaction open the whole time, and doubles the log. Real databases avoid it, and so will you: ALTER TABLE ADD COLUMN changes the catalog and nothing else.

The stage closes Querying because it needs NULL from this part, as the value a new column reads as by default, and because every reader this part built, from the WHERE to GROUP BY, must see the new column.

That works because of a decision made back in Flexible Storage. Every row has stored its own column_count since varlen-rows, even though, until now, it always equalled the table's. Now it can be smaller.

Old rows, new schema

After alter table t add column active int default 1, the catalog says t has three columns, while every row on disk still says it has two. The reader reconciles them: it decodes the columns the row has, and gives every column after them its default.

0
0d
00
00
00
02
00
00
01
00
00
00
00
00
  • row_sizebytes 0–3 (4 bytes) · 13
  • column_countbytes 4–5 (2 bytes) · 2: written before the ALTER
  • null_bitmapbytes 6–6 (1 bytes) · one byte, nothing NULL
  • idbytes 7–10 (4 bytes) · 1
  • name_lenbytes 11–12 (2 bytes) · 0: an empty name, to keep the row short
A row written when t had two columns. The table now has three; the reader sees column_count 2 and gives active its default, 1, without the row ever changing.

The bytes stay as they are until something writes the row. An UPDATE decodes it, defaults included, and writes it back with every column the table has, so the row grows to full width in place. Rows that are never updated stay short forever, and that is fine.

DEFAULT, in three places

A default is the value a column takes when nothing gives it one. It arrives with ALTER because ALTER needs it most, but the same idea already existed in two other places, and now they agree:

  • ADD COLUMN gives the default to every existing row, by the reading rule above.
  • CREATE TABLE can declare defaults: create table p (id int, active int not null default 1).
  • INSERT gives a column it leaves out the column's default. The nulls stage made that NULL; NULL is simply the default of a column that declares none.

A DEFAULT is checked like a value an INSERT would give the column: an INT takes a number, a VARCHAR(n) a string of at most n bytes. The first column, the key, cannot have one (01104): every row needs its own key.

NOT NULL, and when it cannot be added

alter table t add column x int not null on a table with rows would give every existing row a NULL in a column that promises none. That is 01104. With a DEFAULT, not null default 0, the old rows read 0 and the promise holds. On an empty table there is no row to break the promise, so it is allowed, and every later INSERT must give x a value (01503).

A one-page change, and a transactional one

The test that this stage does no rewrite is the transaction counter from Transactions and Recovery. Inside a transaction, ALTER on a table of 300 wide rows, spread over dozens of leaves, changes exactly one page:

droid session
>begin;
Begin statement executed successfully
>alter table w add column x int default 7;
Alter table statement executed successfully
>tx status
[TX] Status: IN_TRANSACTION pages=1
>commit;
Commit statement executed successfully

Because that page is a catalog page like any other, ROLLBACK takes the ALTER back with the rest of the transaction, and the table has its old columns again.

The algorithm in words

1

Add the column

The whole statement is one change to one catalog row.

  1. 1.Check, in the contract's order: the table exists, the type is valid, the name is new, the DEFAULT fits the type and the length, NOT NULL DEFAULT NULL is refused (01503), and a NOT NULL column with no DEFAULT is refused on a table that has a row (01104).
  2. 2.Append the column, with its NOT NULL and its DEFAULT, to the table's column list, and write the catalog row back.
  3. 3.Touch nothing else: no leaf of the table is read or written.
  4. 4.Invariant: inside a transaction, tx status shows one more page changed, the catalog's, however large the table.
2

Read a row that is shorter than its table

Since varlen-rows, every row has stored its column_count. Until now it always equalled the table's.

  1. 1.Decode the columns the row has, as before: the bitmap and the fields for the first column_count columns.
  2. 2.For every column from column_count to the last, give the column's default: its DEFAULT value, or NULL when it has none.
  3. 3.A row with more columns than the table is still 00503: that cannot happen without dropping a column.
  4. 4.Every reader goes through this one decoder, so SELECT, WHERE, the aggregates, btree find and serialize all see the new column.
3

Write rows at full width

Old rows stay short until something writes them.

  1. 1.An INSERT fills every column the statement leaves out with that column's default; a NOT NULL column with no default is still 01503.
  2. 2.An UPDATE decodes the old row, filling in the defaults, applies its assignments, and writes the row with every column the table has. Its key and its place in the tree do not change.

Output contract for this stage

These are the exact lines the tests check. The full definition is on the output contract page, section 50.

InputOutput
alter table t add column active int default 1;Alter table statement executed successfully
catalog list after it[CATALOG] t root=2 (id INT, name VARCHAR(20), active INT DEFAULT 1)
btree find t 2 for a row written before[BTREE] Row: id=2 name='b' active=1
ast alter table t add column active int not null default 1;Statement: ALTER TABLE, Table: t, Add: active int NOT NULL DEFAULT 1
ast create table p (id int, note varchar(5) default 'hi');Columns: [id int, note varchar(5) DEFAULT 'hi']
explain alter table t add column x int;[PLAN] alter table t add column x int, [PLAN] └── AlterTable (table=t)
tx status after an ALTER in a transaction[TX] Status: IN_TRANSACTION pages=1
NOT NULL without DEFAULT on a table with rows; DEFAULT on the first column[ERROR:01104] <message>
not null default null[ERROR:01503] <message>
a column the table already has[ERROR:00408] <message>
a DEFAULT of the wrong type; out of 0 to 4294967295; longer than the column[ERROR:00403], [ERROR:00404], [ERROR:00405] <message>

alter, add, column and default are keywords. The catalog prints NOT NULL and then DEFAULT <v> after a column's type, the value written as a literal.

What you should see when done

shell
$droid --db shop.db
Welcome to droid!
>select * from items;
id | name
1 | lamp
2 | desk
(2 rows)
[EXEC] rows_examined=2
>alter table items add column stock int not null default 0;
Alter table statement executed successfully
>alter table items add column note varchar(20);
Alter table statement executed successfully
>insert into items (id, name, stock) values (3, 'chair', 5);
Insert statement executed successfully (1 rows)
>update items set stock = 2 where id = 1;
Update statement executed successfully (1 rows)
[EXEC] rows_examined=1
>select * from items;
id | name | stock | note
1 | lamp | 2 | NULL
2 | desk | 0 | NULL
3 | chair | 5 | NULL
(3 rows)
[EXEC] rows_examined=3
>alter table items add column sku int not null;
[ERROR:01104] Cannot add a NOT NULL column without a DEFAULT to a table with rows
>.exit
exiting.. good bye!

Row 2 was never touched: it is still two columns long on disk, and reads 0 and NULL from the catalog. Row 1 was updated, so it is now stored at full width. The messages are yours to word; the tests check only the codes.

Common bugs in this stage

  • Rewriting the table. Correct rows, but tx status shows dozens of pages and a large table takes as long as copying it.
  • Old rows read past their end. A decoder that loops over the table's columns reads bytes that are not there. Loop over the row's column_count, then fill in.
  • Defaults applied only to new rows. select * shows NULL for an old row whose column has DEFAULT 1.
  • UPDATE writing the old width. It keeps the row short and loses the value just assigned to the new column.
  • INSERT still writing NULL. A left-out column with a DEFAULT must get the default.
  • The catalog row written but not rolled back. The catalog is pages; the transaction must own the change like any other.
  • NOT NULL checked against the table's size, not its rows. An empty table takes NOT NULL without a default.

Language notes

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

  • C and C++: keep the default as one of your value structs in the column's catalog entry, parsed once when the catalog is loaded, not on every row.
  • Rust: an Option<Value> on each column for its DEFAULT; the decoder can extend the decoded row with the defaults of the remaining columns.
  • Zig: store the default on the column struct; decoding a short row is a loop to column_count followed by a copy of the remaining defaults.

Checklist

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

  • A column added to a populated table reads NULL in the old rows
    old-rows-read-null
  • Old rows read the new column's DEFAULT, and so do new rows that leave it out
    default-padding
  • Old and new rows come back together, in key order
    mixed-width-scan
  • An UPDATE of an old row stores it whole, and it is still found
    update-old-row-widens
  • WHERE, aggregates, GROUP BY and ORDER BY read the new column in old and new rows
    where-and-agg-on-new-col
  • ALTER is checked before anything changes
    add-column-errors
  • NOT NULL with no DEFAULT can be added only to an empty table: 01104
    add-not-null-without-default
  • DEFAULT is checked like a value, and works in CREATE TABLE
    default-values
  • The new column is there in the next session
    alter-persists
  • ALTER inside a transaction rolls back like any DDL
    alter-rollback
  • ALTER changes one page, whatever the size of the table
    no-rewrite
  • catalog list shows NOT NULL and DEFAULT; btree find fills in an old row
    catalog-and-btree
  • alter, add, column and default are keywords; ast and explain show the statement
    tokenize-ast-explain