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.
- 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
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.
- 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
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
nullsstage 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:
>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
Add the column
The whole statement is one change to one catalog row.
- 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.Append the column, with its NOT NULL and its DEFAULT, to the table's column list, and write the catalog row back.
- 3.Touch nothing else: no leaf of the table is read or written.
- 4.Invariant: inside a transaction, tx status shows one more page changed, the catalog's, however large the table.
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.Decode the columns the row has, as before: the bitmap and the fields for the first column_count columns.
- 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.A row with more columns than the table is still 00503: that cannot happen without dropping a column.
- 4.Every reader goes through this one decoder, so SELECT, WHERE, the aggregates, btree find and serialize all see the new column.
Write rows at full width
Old rows stay short until something writes them.
- 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.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.
| Input | Output |
|---|---|
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
$droid --db shop.dbWelcome to droid!>select * from items;id | name1 | lamp2 | 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 | note1 | lamp | 2 | NULL2 | desk | 0 | NULL3 | 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>.exitexiting.. 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 statusshows 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 hasDEFAULT 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 NULLwithout 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 canextendthe 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_countfollowed 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 rowsold-rows-read-null
- Old rows read the new column's DEFAULT, and so do new rows that leave it outdefault-padding
- Old and new rows come back together, in key ordermixed-width-scan
- An UPDATE of an old row stores it whole, and it is still foundupdate-old-row-widens
- WHERE, aggregates, GROUP BY and ORDER BY read the new column in old and new rowswhere-and-agg-on-new-col
- ALTER is checked before anything changesadd-column-errors
- NOT NULL with no DEFAULT can be added only to an empty table: 01104add-not-null-without-default
- DEFAULT is checked like a value, and works in CREATE TABLEdefault-values
- The new column is there in the next sessionalter-persists
- ALTER inside a transaction rolls back like any DDLalter-rollback
- ALTER changes one page, whatever the size of the tableno-rewrite
- catalog list shows NOT NULL and DEFAULT; btree find fills in an old rowcatalog-and-btree
- alter, add, column and default are keywords; ast and explain show the statementtokenize-ast-explain