MoonBase — A zero-FFI, pure-MoonBit reader/writer for the SQLite database file format (version 3)
Pivot note: the project originally started as a from-scratch embedded SQL engine (see git history, 22 early commits). After review feedback about overlap with existing pure-MoonBit engines, it was redirected to the concrete, well-specified SQLite file-format compatibility problem, which is verifiable byte-for-byte against the reference implementation.
| Area | State |
|---|---|
| Reader — header / varint / records / B-tree / schema / query | ✅ implemented, unit-tested |
| Writer — make_db, overflow chains, leaf→interior upgrade, multi-table | ✅ implemented, opens in official sqlite3 |
| Writer — index B-trees (leaf type 10 / interior type 2) | ✅ implemented, PRAGMA integrity_check = ok + planner uses the index |
| Writer — multi-level B-trees (interior pages of interior pages) | ✅ implemented, 100 000 rows verify integrity_check = ok |
| Writer — configurable page size (512 … 65536) | ✅ implemented, incl. the 65536 → 1 header special case |
| Writer — configurable text encoding (UTF-8 / UTF-16LE / UTF-16BE) | ✅ implemented, re-opens in official sqlite3 |
| Writer — composite & UNIQUE indexes | ✅ implemented, IndexSpec::new (multi-column) / IndexSpec::new_unique |
| Structure self-check — verify_db / CLI check | ✅ implemented, catches broken pages / duplicated page refs / orphan pages |
| SQL export — dump-sql | ✅ implemented, replay-verified by official sqlite3 |
| Query — LIKE/IN/BETWEEN, SUM/AVG/MIN/MAX, OFFSET, multi-column ORDER BY | ✅ implemented, unit-tested |
| Query — DISTINCT, ORDER BY … NULLS FIRST|LAST, COUNT(DISTINCT col) | ✅ implemented, unit-tested |
| VACUUM-style compaction — vacuum / CLI vacuum | ✅ implemented, freelist-free output verified by official sqlite3 |
| Unit tests | ✅ 59 passing (moon test) |
| CI | ✅ GitHub Actions (moon check / moon test / js + wasm-gc builds) |
| License | ✅ Apache-2.0 (LICENSE) |
| Cross-backend | ✅ pure MoonBit, zero FFI (js / wasm-gc / wasm / native) |
| Reviewer concern | How MoonBase answers it |
|---|---|
| Compatibility target too vague | The target is precisely the SQLite Database File Format, version 3 — a documented, versioned binary format (header magic SQLite format 3\0, schema_format 1–4, text_encoding UTF-8/16). We verify against real sqlite3 output, and our writer's output opens in the official sqlite3. |
| Transaction / persistence boundary unclear | MoonBase is a format tool. The reader operates over committed pages (no WAL/rollback/durability). The writer materialises a fresh, fully-committed single-file database in one pass — there is no half-written state and no durability claim. The boundary is crisp. |
| Overlap with existing engines | Unlike FFI bindings (sqlite3.mbt, which shells out to the C library) we use zero FFI. Unlike self-format engines (otter/moonlake, which invent their own file layout) we target the industry-standard SQLite format. Unlike the pure-MoonBit read-only inspector prowk/MoonSQLiteFile (which explicitly states it does not write files / does not run SQL) MoonBase is write-capable — generating valid .db files is our headline non-overlapping contribution. The broader pure-MoonBit binary-format ecosystem (BSON, MsgPack, OCaml Marshal, CAN DBC, TTF, CSV) proves the ecosystem values zero-FFI format libraries, but none of them cover the SQLite on-disk format, let alone generating valid .db — MoonBase fills that gap. We are an interop layer, not a competing engine. See 查重报告_MoonBase.md for the full cross-version audit. |
moonbase/
├── src/sqlite/ Pure-MoonBit SQLite file-format layer (zero FFI)
│ ├── value.mbt Value model, DbError, SQLite collation (NULL<num<text<blob)
│ ├── varint.mbt SQLite varint encode/decode (7-bit groups, ≤9 bytes)
│ ├── pager.mbt 100-byte DB header + page slicing / usable size
│ ├── record.mbt Record serial types: int(1..6), float64, text(UTF-8/16), blob, const 0/1
│ ├── btree_file.mbt B-tree page parse (leaf/interior table & index), overflow chains
│ ├── lexer.mbt Tolerant DDL lexer for schema text inside the file
│ ├── schema.mbt sqlite_master scan + CREATE TABLE parser (rowid alias, WITHOUT ROWID)
│ ├── db.mbt Db facade: open, table lookup, row materialisation
│ ├── query.mbt Read-only SELECT: projection, COUNT(*), WHERE, ORDER BY, LIMIT (3-valued logic)
│ ├── writer.mbt **Writer**: make_db — generate valid .db from scratch (header, records, leaves, overflow chains, sqlite_master)
│ ├── writer_tree.mbt Writer: bottom-up multi-level table & index B-tree construction, page arena, page-size fan-out
│ ├── verify.mbt Structure self-check: page ownership, page types, key ordering, overflow chains, freelist
│ ├── export.mbt Export a whole database as a replayable SQL script (export_sql)
│ ├── value_wbtest.mbt 10 reader unit tests
│ ├── writer_wbtest.mbt 14 writer tests (round-trip, float64, overflow, multi-leaf, multi-table, index ordering/separators, 3-level trees, page sizes)
│ ├── verify_wbtest.mbt 5 self-check tests (clean db, overflow, multi-level, two corruption cases)
│ └── export_wbtest.mbt 6 export / SQL-literal tests
└── cmd/moonbase-cli/ CLI demo (js backend; Node fs for real file I/O)| Component | Feature | Status |
|---|---|---|
| Header | 100-byte header: page_size (1→65536), versions, encodings, schema_cookie, sqlite_version | ✅ |
| Varint | Decode + encode, 9-byte edge case (1<<56) round-trips | ✅ |
| Record | Serial types 0/1–6/7/8/9, signed ints, float64, UTF-8 & UTF-16 text, BLOB | ✅ |
| B-tree | leaf-table / interior-table / leaf-index / interior-index; local-payload formula; overflow chains with cycle guard | ✅ |
| Schema | sqlite_master scan; CREATE TABLE parser tolerant of constraints; INTEGER PRIMARY KEY → rowid alias; WITHOUT ROWID | ✅ |
| Query | SELECT *, column projection + AS, COUNT(*)/COUNT(col)/COUNT(DISTINCT col), SUM/AVG/MIN/MAX, WHERE (= <> < <= > >= AND OR NOT IS [NOT] NULL + LIKE/NOT LIKE/IN/NOT IN/BETWEEN), ORDER BY … ASC|DESC [NULLS FIRST|LAST] (multi-column), LIMIT n [OFFSET m], DISTINCT (row-level dedup) | ✅ |
| Collation | Code-point-wise text compare (matches SQLite, not MoonBit's length-first order) | ✅ |
| Writer | make_db(Tables) materialises a valid .db from scratch in pure MoonBit: 100-byte header, leaf/interior table B-trees, records, overflow-page chains, sqlite_master rows; INTEGER PRIMARY KEY rowid aliasing, auto rowid, multi-table. Output re-opens in official sqlite3. | ✅ |
| Writer — indexes | IndexSpec / TableSpec::with_indexes emit real index B-trees (leaf type 10, interior type 2): keys sorted with SQLite semantics (NULL < number < text < BLOB, rowid tie-break), separators promoted out of leaves, payload overflow supported. Verified by PRAGMA integrity_check and by the planner choosing the index. | ✅ |
| Writer — multi-level trees | Tables and indexes are packed bottom-up into as many interior levels as needed; a separator key is always removed from the page below (index B-trees) or copied (table B-trees), matching SQLite's two different key semantics. 20 000 rows on 512-byte pages produce a 3-level table tree and a 4-level index tree, still integrity_check = ok. | ✅ |
| Writer — page size | make_db_with_page_size(tables, 512…65536) — header, usable space, overflow thresholds and fan-out all follow the chosen page size; 65536 is encoded as 1 in the 2-byte header field, as the spec requires. | ✅ |
| Writer — text encoding | make_db_with_encoding(tables, page_size, 1|2|3) — generates UTF-8 / UTF-16LE / UTF-16BE databases (header byte 56 + serial-type-13 bodies); re-opens in official sqlite3 with the correct PRAGMA encoding and integrity_check = ok. | ✅ |
| Writer — composite & UNIQUE indexes | IndexSpec::new(name, table, sql, ["a","b"]) builds multi-column index keys (a,b,rowid); IndexSpec::new_unique emits a UNIQUE index recognised by parse_index on read-back. | ✅ |
| Writer — sorting | Row and index ordering uses a stable bottom-up merge sort (O(n log n)), so 100 000-row files build in ~1 s instead of degrading quadratically. | ✅ |
| Self-check | verify_db(db) / moonbase-cli check: walks every tree itself and validates file length vs page count, page ownership (no page referenced twice), page types vs tree kind, strict key/rowid ordering, overflow-chain completeness, freelist and orphan pages. Works on files produced by official sqlite3 too (freelist, overflow, WITHOUT ROWID). | ✅ |
| VACUUM-style compaction | vacuum(db, page_size) / moonbase-cli vacuum <in> <out> [page] rebuilds a database into a compact freelist-free equivalent, preserving rows (and rowids / INTEGER PRIMARY KEY aliases) and indexes. A 177-page official-sqlite3 database with a non-empty freelist compacts to 89 pages with integrity_check = ok and freelist_count = 0. WITHOUT ROWID is rejected with Unsupported. | ✅ |
| Export | export_sql / CLI dump-sql: dump a whole database as a SQL script (CREATE + INSERT + CREATE INDEX + COMMIT); the official sqlite3 replays it to identical rows (PRAGMA integrity_check = ok). | ✅ |
| FFI | None — pure MoonBit, cross-compiles to js / wasm / native | ✅ |
| Tests | 59 unit tests (10 reader + 14 writer + 5 self-check + 6 export + 16 query + 4 encoding/unique + 4 vacuum); writer output verified re-readable by the in-tree reader and by official sqlite3 fixtures | ✅ |
# Run the test suite (works on default backend — no FFI needed)
moon test
# Build the CLI demo (JavaScript backend, runs on Node.js — real file I/O via Node fs)
moon build --target js
node _build/js/debug/build/cmd/moonbase-cli/moonbase-cli.js
# Build for other backends (CLI uses a stub there; the library is fully cross-target)
moon build # default backend, builds & tests cleanmoonbase-cli header <file.db> print the 100-byte database header
moonbase-cli schema <file.db> list tables / indexes / columns
moonbase-cli tree <file.db> <table> show the table B-tree root + row count
moonbase-cli query <file.db> "SELECT ..." run a read-only query
moonbase-cli dump <file.db> <table> export the whole table (pipe-separated)
moonbase-cli dump-sql <file.db> export the whole database as a SQL script
moonbase-cli index <file.db> <index> list every entry of an index (key… + rowid)
moonbase-cli check <file.db> structural self-check (no sqlite3 needed)
moonbase-cli vacuum <in.db> <out.db> [page] compact a database (drop freelist / dead space)
moonbase-cli make <out.db> [rows] [page] [enc] generate a legal .db from scratch, with an index
enc: 1=UTF-8 (default), 2=UTF-16le, 3=UTF-16be// ---- read ----
let db = @sqlite.Db::open(bytes) // bytes: Bytes of a .db file
let t = db.table("users")
for row in db.rows(t) { /* row.values : Array[Value] */ }
let rs = @sqlite.query(db, "SELECT name FROM users ORDER BY name")
// ---- write: generate a brand-new .db from scratch ----
let rows : Array[@sqlite.RowSpec] = [
@sqlite.RowSpec::new(None, [
@sqlite.VInt(Int64::from_int(1)),
@sqlite.VText("Alice"),
]),
]
let tables : Array[@sqlite.TableSpec] = [
@sqlite.TableSpec::new(
"users",
"CREATE TABLE users(id integer primary key, name text)",
rows,
),
]
let out_bytes = @sqlite.make_db(tables) // page size 4096, UTF-8
let small = @sqlite.make_db_with_page_size(tables, 512) // 512 .. 65536
let utf16 = @sqlite.make_db_with_encoding(tables, 4096, 2) // 1=UTF-8, 2=UTF-16le, 3=UTF-16be
// ---- self-check: walk the file structure without sqlite3 ----
let report = @sqlite.verify_db(db)
if !report.passed() { for p in report.problems() { println(p) } }
// ---- export: dump the whole DB as a replayable SQL script ----
let sql = @sqlite.export_sql(db)| Document | Contents |
|---|---|
| docs/USAGE.md | Full usage guide: CLI, read API, writer API, self-check, export, limitations |
| docs/FORMAT_NOTES.md | Byte-level notes: page layout, cell formats, separator semantics, multi-level packing, self-check invariants |
| CHANGELOG.md | Release history and notable fixes |
# create a fixture with the official tool
python -c "import sqlite3; c=sqlite3.connect('demo.db'); \
c.execute('create table t(id integer primary key, name text, score real)'); \
c.executemany('insert into t values(?,?,?)', [(1,'Alice',91.5),(2,'Bob',88.0),(3,'陈小明',77.25)]); \
c.commit()"
node _build/js/debug/build/cmd/moonbase-cli/moonbase-cli.js schema demo.db
# table t (root=2, 3 列)
# id integer [INTEGER PRIMARY KEY -> rowid 别名]
# name text
# score real
node _build/js/debug/build/cmd/moonbase-cli/moonbase-cli.js query demo.db \
"SELECT name, score FROM t WHERE score > 80 ORDER BY score DESC"
# name|score
# Alice|91.5
# Bob|88# Build the CLI (js backend) and generate a legal .db from scratch
moon build --target js
node _build/js/debug/build/cmd/moonbase-cli/moonbase-cli.js make demo_out.db
# generated demo_out.db (users: id INTEGER PK, name TEXT, score REAL, memo BLOB)
# (a) the in-tree reader can read it back
node _build/js/debug/build/cmd/moonbase-cli/moonbase-cli.js query demo_out.db \
"SELECT name, score FROM users ORDER BY score DESC"
# (b) the OFFICIAL sqlite3 can open it too — proof it is a real SQLite file
python -c "import sqlite3; c=sqlite3.connect('demo_out.db'); \
print(c.execute('SELECT name, score FROM users ORDER BY score DESC').fetchall())"Install
Download zipMoonBase — A zero-FFI, pure-MoonBit reader/writer for the SQLite database file format (version 3)