DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
sqlite_format.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sqlite_format.h
6 * @brief Reader for the SQLite3 on-disk file format (DETWS_ENABLE_SQLITE).
7 *
8 * This is **file-format access**, not the SQLite library: the documented database file structure
9 * (https://www.sqlite.org/fileformat2.html) parsed by hand - the 100-byte database header, the b-tree
10 * page header, the record varint, and record serial types. It is read-first (a bounded writer is a later
11 * step), pure (no I/O; you hand it page bytes), and host-testable against real files produced by the
12 * sqlite3 CLI. It deliberately does not pull in the SQLite amalgamation, which needs a heap and stdio and
13 * does not fit the no-stdlib zero-heap model.
14 *
15 * A caller reads a page (::SqliteDbHeader::page_size bytes) from the backing store - e.g. via wal_fs /
16 * fs::FS - and walks it with these parsers: page 1 begins with the database header (100 bytes) immediately
17 * followed by the root b-tree page header of the schema table.
18 */
19
20#ifndef DETERMINISTICESPASYNCWEBSERVER_SQLITE_FORMAT_H
21#define DETERMINISTICESPASYNCWEBSERVER_SQLITE_FORMAT_H
22
23#include "ServerConfig.h"
24#include <stddef.h>
25#include <stdint.h>
26
27#if DETWS_ENABLE_SQLITE
28
29/**
30 * @brief Decode a SQLite variable-length integer (1-9 bytes, big-endian; the high bit of each of the first
31 * 8 bytes is a continuation flag, the 9th byte contributes all 8 bits).
32 * @return the number of bytes consumed (1-9), or 0 if @p len is too short for a complete varint.
33 */
34size_t sqlite_varint_decode(const uint8_t *buf, size_t len, uint64_t *out);
35
36/**
37 * @brief Content byte size of a record column with the given record serial type.
38 *
39 * 0 -> NULL (0), 1..6 -> 1/2/3/4/6/8-byte ints, 7 -> 8-byte float, 8/9 -> the constants 0/1 (0 bytes),
40 * N>=12 even -> BLOB of (N-12)/2 bytes, N>=13 odd -> TEXT of (N-13)/2 bytes. Reserved 10/11 -> 0.
41 */
42uint64_t sqlite_serial_type_size(uint64_t serial_type);
43
44/** @brief Parsed subset of the 100-byte database header (all fields are big-endian on media). */
45struct SqliteDbHeader
46{
47 uint32_t page_size; ///< 512..65536 (the on-disk value 1 means 65536)
48 uint8_t write_version; ///< 1 = legacy (rollback journal), 2 = WAL
49 uint8_t read_version; ///< 1 = legacy, 2 = WAL
50 uint8_t reserved_per_page; ///< reserved bytes at the end of each page
51 uint32_t file_change_counter; ///< bumped on each transaction
52 uint32_t page_count; ///< database size in pages (valid when it matches version-valid-for)
53 uint32_t freelist_first; ///< first freelist trunk page (0 = none)
54 uint32_t freelist_count; ///< number of freelist pages
55 uint32_t schema_cookie; ///< schema version cookie
56 uint32_t schema_format; ///< schema format number (1..4)
57 uint32_t text_encoding; ///< 1 = UTF-8, 2 = UTF-16le, 3 = UTF-16be
58 uint32_t user_version; ///< user_version pragma
59 uint32_t application_id; ///< application_id pragma
60 uint32_t sqlite_version; ///< SQLITE_VERSION_NUMBER that last wrote the file
61};
62
63/**
64 * @brief Parse and validate the 100-byte database header at @p buf (@p len must be >= 100).
65 * @return false if the magic string is wrong or the page size is not a valid power of two.
66 */
67bool sqlite_parse_db_header(const uint8_t *buf, size_t len, SqliteDbHeader *out);
68
69/** @brief B-tree page types (the first byte of a b-tree page header). */
70// SQLite b-tree page types (the page-header first byte): wire/format values compared, so integer
71// constants in a namespacing struct.
72struct SqliteBtree
73{
74 static constexpr uint8_t SQLITE_BTREE_INTERIOR_INDEX = 2;
75 static constexpr uint8_t SQLITE_BTREE_INTERIOR_TABLE = 5;
76 static constexpr uint8_t SQLITE_BTREE_LEAF_INDEX = 10;
77 static constexpr uint8_t SQLITE_BTREE_LEAF_TABLE = 13;
78};
79
80/** @brief Parsed b-tree page header (8 bytes for a leaf, 12 for an interior page). */
81struct SqliteBtreeHeader
82{
83 uint8_t type; ///< one of the SQLITE_BTREE_* values
84 uint16_t first_freeblock; ///< offset of the first freeblock (0 = none)
85 uint16_t cell_count; ///< number of cells on the page
86 uint32_t cell_content_start; ///< start of the cell content area (on-disk 0 means 65536)
87 uint8_t frag_free_bytes; ///< fragmented free bytes in the cell content area
88 uint32_t right_most_page; ///< right-most child page (interior pages only; 0 on a leaf)
89 uint8_t header_size; ///< 8 (leaf) or 12 (interior) - where the cell pointer array begins
90};
91
92/**
93 * @brief Parse a b-tree page header located at @p page + @p offset (@p offset is 100 for page 1, else 0).
94 * @param page_len bounds the read. @return false if the page type byte is not a valid b-tree type or the
95 * header runs past @p page_len.
96 */
97bool sqlite_parse_btree_header(const uint8_t *page, size_t page_len, size_t offset, SqliteBtreeHeader *out);
98
99/**
100 * @brief In-page byte offset of the @p i-th cell (0-based) from the cell pointer array. @return 0 if @p i is
101 * out of range or the pointer array runs past @p page_len. @p page_offset is 100 for page 1, else 0.
102 */
103uint32_t sqlite_cell_pointer(const uint8_t *page, size_t page_len, const SqliteBtreeHeader *bh, size_t page_offset,
104 uint16_t i);
105
106/** @brief A parsed table-b-tree leaf cell (a table row): its rowid and where its record payload lives. */
107struct SqliteTableLeafCell
108{
109 uint64_t rowid;
110 uint32_t payload_len; ///< total record payload length
111 uint32_t local_off; ///< in-page offset where the record payload begins
112 uint32_t local_len; ///< payload bytes stored in this page (== payload_len unless it overflows)
113 bool has_overflow; ///< true when payload_len > local_len (remainder is on overflow pages)
114};
115
116/**
117 * @brief Parse the leaf-table cell at in-page offset @p cell_off (payload-length varint, then rowid varint).
118 *
119 * Computes the local payload extent using the SQLite overflow threshold (@p page_size and @p reserved size
120 * the usable area). Reading the overflow-page chain is a follow-up; @c has_overflow tells you when the
121 * record is only partially present in this page. @return false on a truncated/invalid cell.
122 */
123bool sqlite_parse_table_leaf_cell(const uint8_t *page, size_t page_len, uint32_t page_size, uint8_t reserved,
124 uint32_t cell_off, SqliteTableLeafCell *out);
125
126/** @brief Cursor over the columns of a record (row payload): the header varints and the value bytes. */
127struct SqliteRecordCursor
128{
129 const uint8_t *rec;
130 uint32_t rec_len;
131 uint32_t hdr_pos; ///< offset of the next serial-type varint within the header
132 uint32_t hdr_end; ///< end of the record header (start of the value area)
133 uint32_t val_pos; ///< offset of the next column value
134};
135
136/** @brief Begin a record cursor over @p rec_len bytes at @p rec. @return false if the header is malformed. */
137bool sqlite_record_begin(SqliteRecordCursor *c, const uint8_t *rec, uint32_t rec_len);
138
139/**
140 * @brief Advance to the next column. Sets @p serial_type and points @p val / @p val_len at the value bytes
141 * (0-length for NULL and the integer constants 0/1). @return false when there are no more columns.
142 */
143bool sqlite_record_next(SqliteRecordCursor *c, uint64_t *serial_type, const uint8_t **val, uint32_t *val_len);
144
145/** @brief Decode an integer column value (serial types 1-6, and 8/9 -> 0/1), sign-extended big-endian. */
146int64_t sqlite_column_int(uint64_t serial_type, const uint8_t *val, uint32_t val_len);
147
148/** @brief Decode a float column value (serial type 7): an 8-byte big-endian IEEE-754 double. */
149double sqlite_column_float(const uint8_t *val, uint32_t val_len);
150
151/** @brief Maximum table b-tree depth the cursor descends (SQLite trees are shallow; this is generous). */
152#define SQLITE_BTREE_MAX_DEPTH 20
153
154/**
155 * @brief Fetch page number @p pgno (1-based) into @p page (@p page_size bytes). @return true on success.
156 * The table cursor pulls pages through this so it works over any backing store (a RAM image, wal_fs, fs::FS).
157 */
158using SqlitePageReader = bool (*)(void *ctx, uint32_t pgno, uint8_t *page, uint32_t page_size);
159
160/**
161 * @brief Reassemble a row's full record payload, following the overflow-page chain (fileformat2.html 1.6).
162 *
163 * A row that does not fit its leaf page keeps its first @c local_len bytes in the leaf, followed by a 4-byte
164 * big-endian first-overflow-page number; each overflow page is a 4-byte next-page pointer (0 = last) plus up
165 * to `page_size - reserved - 4` content bytes. This copies the in-page prefix, then walks the chain until
166 * @c payload_len bytes are gathered.
167 *
168 * @param read/ctx page source (the same reader the table cursor uses).
169 * @param page_size page size in bytes; @p reserved the per-page reserved tail (usable = page_size - reserved).
170 * @param leaf_page the leaf page the cell was parsed from (holds the local prefix + the overflow pointer).
171 * @param cell the parsed leaf cell.
172 * @param out destination for the full payload; must hold at least @c payload_len bytes.
173 * @param out_cap capacity of @p out - a payload larger than this fails closed (no overrun).
174 * @param work_page a @p page_size scratch buffer for reading overflow pages (must not alias @p leaf_page).
175 * @return true when @c payload_len bytes were reassembled into @p out; false on a short buffer, a read error,
176 * or a broken / looping chain (the page count is bounded, so a corrupt pointer cannot spin forever).
177 */
178bool sqlite_read_payload(SqlitePageReader read, void *ctx, uint32_t page_size, uint8_t reserved,
179 const uint8_t *leaf_page, const SqliteTableLeafCell *cell, uint8_t *out, uint32_t out_cap,
180 uint8_t *work_page);
181
182/**
183 * @brief A forward cursor over the rows of a table b-tree, in rowid order, across pages.
184 *
185 * It walks the interior/leaf table b-tree rooted at a table's `rootpage`, descending leftmost and yielding
186 * each leaf cell as a row. It keeps only a bounded descent stack of page numbers (re-reading an interior
187 * page when it returns to it) plus two page buffers - the current leaf and a scratch - so memory is fixed
188 * regardless of table size. A row that overflows onto overflow pages yields only its in-page prefix by
189 * default (and @c has_overflow flags it); provide an overflow buffer with
190 * ::sqlite_table_cursor_set_overflow_buf and the cursor transparently reassembles the full record for each
191 * overflowing row instead.
192 */
193struct SqliteTableCursor
194{
195 SqlitePageReader read;
196 void *ctx;
197 uint32_t page_size;
198 uint8_t reserved;
199 uint8_t *leaf; ///< page_size buffer holding the current leaf page (values point into it)
200 uint8_t *work; ///< page_size scratch for interior-page reads during traversal
201 uint8_t *ovf_buf; ///< optional buffer to reassemble overflowing rows into (null = prefix-only)
202 uint32_t ovf_cap; ///< capacity of ovf_buf in bytes
203 uint32_t stack_pg[SQLITE_BTREE_MAX_DEPTH];
204 uint16_t stack_idx[SQLITE_BTREE_MAX_DEPTH]; ///< next child index to descend at each interior frame
205 int depth;
206 SqliteBtreeHeader leaf_hdr;
207 uint32_t leaf_off;
208 uint32_t leaf_pgno;
209 uint16_t leaf_cell; ///< next cell index within the current leaf
210 uint16_t leaf_count; ///< cells in the current leaf
211};
212
213/**
214 * @brief Opt in to full overflow-chain reassembly. @p buf (@p cap bytes, must outlive the cursor and be at
215 * least as large as the biggest row) receives the reassembled record for each overflowing row; without it
216 * ::sqlite_table_cursor_next yields only the in-page prefix of an overflowing row. Reuses the cursor's work
217 * page as scratch, so it adds no other buffer. Call after ::sqlite_table_cursor_begin.
218 */
219void sqlite_table_cursor_set_overflow_buf(SqliteTableCursor *c, uint8_t *buf, uint32_t cap);
220
221/**
222 * @brief Begin a table cursor at @p rootpage. @p leaf_buf and @p work_buf are each @p page_size bytes and
223 * must outlive the cursor. @return false if the root page cannot be read/parsed as a table b-tree.
224 */
225bool sqlite_table_cursor_begin(SqliteTableCursor *c, SqlitePageReader read, void *ctx, uint32_t page_size,
226 uint8_t reserved, uint32_t rootpage, uint8_t *leaf_buf, uint8_t *work_buf);
227
228/**
229 * @brief Advance to the next row: sets @p rowid and starts @p row (a record cursor over the row's columns).
230 * Column value pointers stay valid until the next call. @return false at the end of the table.
231 */
232bool sqlite_table_cursor_next(SqliteTableCursor *c, uint64_t *rowid, SqliteRecordCursor *row);
233
234// ---------------------------------------------------------------------------
235// Writer (bounded): build a fresh single-table SQLite database image.
236//
237// The inverse of the reader: it emits a byte-valid SQLite3 file that the sqlite3 CLI (or any reader) can
238// open. It is deliberately BOUNDED - it lays the schema row on page 1 and the table's rows into one leaf
239// b-tree page on page 2, and fails closed if a row would overflow the page or the rows do not fit (no page
240// splitting, no overflow pages, no interior maintenance). That covers the on-device small config / dataset
241// export case; a multi-page writer is a follow-up. Zero-heap: it writes straight into the caller's buffer.
242// ---------------------------------------------------------------------------
243
244/** @brief Encode a SQLite varint for @p v into @p out. @return bytes written (1-9), or 0 if @p cap too small. */
245size_t sqlite_varint_encode(uint64_t v, uint8_t *out, size_t cap);
246
247/** @brief Column value kind for the record writer. */
248enum class SqliteColType : uint8_t
249{
250 SQLITE_COL_NULL,
251 SQLITE_COL_INT,
252 SQLITE_COL_FLOAT,
253 SQLITE_COL_TEXT,
254 SQLITE_COL_BLOB
255};
256
257/** @brief A typed column value to write (TEXT/BLOB bytes are referenced, not copied, until encode time). */
258struct SqliteValue
259{
260 SqliteColType type;
261 int64_t i; ///< INT value
262 double f; ///< FLOAT value
263 const uint8_t *data; ///< TEXT / BLOB bytes
264 uint32_t len; ///< TEXT / BLOB byte length
265};
266
267/**
268 * @brief Encode @p n columns as a record (row payload): the header (a length varint + one serial-type varint
269 * per column) followed by the value bytes. Integer columns use the minimal serial type (0/1 -> the 8/9
270 * constants, else a 1/2/3/4/6/8-byte big-endian int); TEXT/BLOB use 13+2n / 12+2n. @return bytes written, or
271 * 0 on overflow / bad input.
272 */
273uint32_t sqlite_encode_record(const SqliteValue *cols, uint32_t n, uint8_t *out, uint32_t out_cap);
274
275/** @brief A row for the table builder: its rowid and its column values (rowids must be ascending). */
276struct SqliteRow
277{
278 uint64_t rowid;
279 const SqliteValue *cols;
280 uint32_t ncols;
281};
282
283/**
284 * @brief Build a fresh two-page single-table database (page 1 = database header + the `sqlite_schema` row,
285 * page 2 = the table's leaf b-tree) into @p out.
286 *
287 * @param page_size 512..65536, a power of two.
288 * @param table_name the table's name (stored as `sqlite_schema.name` and `.tbl_name`).
289 * @param create_sql the exact `CREATE TABLE ...` text stored in `sqlite_schema.sql`.
290 * @param rows the table rows, rowid-ascending; @p nrows may be 0 (empty table).
291 * @param out destination image buffer; @p out_cap must be at least 2 * @p page_size bytes.
292 * @return the image length (2 * @p page_size), or 0 if a row would overflow a page, the rows do not fit one
293 * leaf page (the bounded writer fails closed), or on bad input.
294 */
295uint32_t sqlite_build_table_db(uint32_t page_size, const char *table_name, const char *create_sql,
296 const SqliteRow *rows, uint32_t nrows, uint8_t *out, uint32_t out_cap);
297
298#endif // DETWS_ENABLE_SQLITE
299#endif // DETERMINISTICESPASYNCWEBSERVER_SQLITE_FORMAT_H
User-facing configuration for DeterministicESPAsyncWebServer.