Accept PRIMARY KEY on any INT column and key the table's B+Tree by it; keep an INT first column as the key when nothing is marked; and give every other table a hidden, ever-increasing rowid that the catalog remembers.
Since the B+Tree stages, a table's rows have lived in one tree, ordered by the first column, which had to be an INT. That rule was a shortcut: the engine needed a key, and the first column was the easy one to find. It is also wrong in two ways that the next stages would trip over.
First, the key is a property of the data, not of the column order. create table orders (customer varchar(20), id int primary key) is a perfectly good table whose key is its second column. Second, many tables have no natural key at all: a log of events, a list of names. Forcing the user to invent an INT column for them is what this stage removes.
The indexes of this part make the choice matter more. A secondary index stores, for each row, the indexed value and the row's key, and finds the row by looking that key up in the table's tree. So the key has to be settled, stable and known for every table before the first index exists. That is this stage.
Every table has exactly one key, decided by CREATE TABLE and never changed:
PRIMARY KEY, in any position. It must be INT, there is only one, and it takes no DEFAULT.Whatever the case, the key does the same jobs it always did: the tree is ordered by it, an INSERT must have one that the tree does not hold yet, an UPDATE cannot change it, and a WHERE bound on it is an IndexScan. Only where it comes from differs.
The rowid lives where every key lives: in the cell, in front of the row. The row itself holds only the declared columns. That is why select * does not show it, and why the catalog's column list does not mention it.
It can still be read. On a rowid table the name rowid means the row's key, wherever a column could be read: select rowid, name from h, where rowid = 2, order by rowid. It cannot be written: an INSERT or UPDATE that names it is 00402, because it is not a column. On a table keyed by a column, rowid is not a name at all.
The obvious way to pick the next rowid is to look at the largest key in the tree and add one. It is also wrong for this stage: delete the last row, insert a new one, and the new row gets the deleted row's rowid. Anything outside the table that remembered the old rowid now points at a different row. droid promises that a rowid is never given twice.
So the next rowid is stored, as a fifth column of the table's catalog row, next_rowid. Every INSERT into a rowid table reads it, uses it, and writes it back one higher. That puts the counter on a page like any other, and everything you built for pages already handles it:
$droid --db log.dbWelcome to droid!>create table h (name varchar(20), city varchar(10));Create table statement executed successfully>insert into h (name, city) values ('ada', 'x');Insert statement executed successfully (1 rows)>insert into h (name, city) values ('bob', 'x');Insert statement executed successfully (1 rows)>delete from h where rowid = 2;Delete statement executed successfully (1 rows)[EXEC] rows_examined=1>insert into h (name, city) values ('cy', 'x');Insert statement executed successfully (1 rows)>select rowid, name from h;rowid | name1 | ada3 | cy(2 rows)[EXEC] rows_examined=2
The choice is made once, from the column list, and never changes.
Every place that took the first column as the key now takes the key column.
rowid is resolved like a column name, but only for reading.
These are the exact lines the tests check. The full definition is on the output contract page, sections 52 and 53.
| Input | Output |
|---|---|
tokenize id int primary key | [IDENTIFIER - id], [IDENTIFIER - int], [KEYWORD_PRIMARY - primary], [KEYWORD_KEY - key] |
ast create table t (name varchar(20), id int primary key not null); | Columns: [name varchar(20), id int NOT NULL PRIMARY KEY] |
catalog list | [CATALOG] t root=2 (name VARCHAR(20), id INT PRIMARY KEY, n INT); a rowid table prints its columns with no marker |
select * from h; on a rowid table | the declared columns only |
select rowid, name from h; | rowid | name, then 1 | ada, 2 | bob, ... |
explain select * from t where id = 2; | [PLAN] └── IndexScan (table=t, key=2), for a key column in any position |
explain select name from h where rowid = 2; | Project (cols=[name]) over IndexScan (table=h, key=2) |
| a PRIMARY KEY on a VARCHAR column | [ERROR:01103] <message> |
| two columns marked PRIMARY KEY; ADD COLUMN with PRIMARY KEY | [ERROR:01601] <message> |
| a DEFAULT on the key column | [ERROR:01104] <message> |
| INSERT without the key column; NULL in it; a repeated key; UPDATE of it | 00406, 01503, 00701, 00801, as for the first column before |
rowid in an INSERT column list or an UPDATE assignment, or on a table keyed by a column | [ERROR:00402] <message> |
primary and key are keywords. rowid is not: it is an identifier that a rowid table resolves.
$droid --db shop.dbWelcome to droid!>create table orders (customer varchar(20), id int primary key);Create table statement executed successfully>insert into orders (customer, id) values ('cy', 30);Insert statement executed successfully (1 rows)>insert into orders (customer, id) values ('ada', 10);Insert statement executed successfully (1 rows)>insert into orders (customer, id) values ('bob', 10);[ERROR:00701] key 10 already exists>select * from orders;customer | idada | 10cy | 30(2 rows)[EXEC] rows_examined=2>create table notes (body varchar(40));Create table statement executed successfully>insert into notes (body) values ('first');Insert statement executed successfully (1 rows)>insert into notes (body) values ('second');Insert statement executed successfully (1 rows)>select rowid, body from notes;rowid | body1 | first2 | second(2 rows)[EXEC] rows_examined=2>catalog list[CATALOG] Tables: 2[CATALOG] orders root=2 (customer VARCHAR(20), id INT PRIMARY KEY)[CATALOG] notes root=3 (body VARCHAR(40))>.exitexiting.. good bye!
The messages are yours to word; the tests check only the codes.
btree find) has to ask the table which column is its key.select * shows it, and every row is four bytes larger than its columns. It is the cell's key, not a value.The page above is the same for every language. These are the only language-specific hints.
std::optional<std::size_t> for the key column says "rowid" by being empty.Key { Column(usize), RowId } makes the compiler find every place that still assumes column 0.switch exhaustiveness.