Row encoding

How a row of values becomes bytes on a page.

A row in memory is a list of values. On a page it has to be a run of bytes. Row encoding is the rule that turns one into the other and back.

Fishbowl writes the number of columns, then each value as a one-byte type tag followed by its bytes:

TagTypeBytes after the tag
0NULLnone
1INTEGER8, big-endian
2REAL8, an IEEE 754 double
3TEXTa 4-byte length, then the UTF-8 text
4BLOBa 4-byte length, then the bytes

So (7, NULL, 'hi') becomes 19 bytes:

00 03                         3 columns
01 00 00 00 00 00 00 00 07    INTEGER 7
00                            NULL
03 00 00 00 02 68 69          TEXT, 2 bytes: "hi"

The tag is needed because a SQLite column can hold any type, so the bytes must say which type each value is. See types and affinity.

What SQLite does differently

SQLite puts all the type tags first, in a header, and the values after. Its tags also encode the size, so it can jump straight to column 5 without reading columns 1 to 4. It also stores small integers in fewer bytes. Fishbowl keeps tag and value side by side and integers at 8 bytes. That uses more space, but a row is easy to read in a hex dump, which matters for a database built to be looked at.

Rows bigger than a page

A row of 10,000 characters cannot fit in a 4 KiB page. Rows over 1,000 bytes go to overflow pages instead. The leaf keeps the rowid, the total length and the number of the first overflow page. Each overflow page holds about 4 KiB of the row and the number of the next one, like a chain.

The 1,000-byte limit means a leaf always fits at least four rows. That guarantees a full leaf can be split into two halves that both fit. See splits and merges.

Code: crates/engine/src/storage/record.rs (rows) and crates/engine/src/storage/btree.rs (store_payload, overflow chains).