Sign in

    moonsqlitefile

    Pure MoonBit read-only SQLite file format parser and inspector

    sqlite
    parser
    database
    inspector
    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    11 days ago
    Downloads
    145

    #MoonSQLiteFile

    CI Mooncakes License

    纯 MoonBit 的 SQLite 文件解析与检查库

    MoonSQLiteFile 直接读取 SQLite 3 数据库的磁盘格式,提供文件头、页面、schema 和原始记录的只读 API。适合构建离线数据库浏览器、文件结构检查工具,以及 SQLite 存储格式的教学演示。

    核心库只依赖 MoonBit 标准库,支持 Wasm、WasmGC、JavaScript 和 native,无需 SQLite 引擎或 FFI。文件 I/O 由宿主提供;随附的 Node.js CLI 可直接检查本地数据库快照。

    #功能

    • 文件与页面解析:100 字节文件头、512–65536 字节页面、四类 B-tree 页、cell pointer 和 freeblock 链。
    • 记录解码:1–9 字节 varint、标准 serial types、64 位整数、浮点、NULL、BLOB,以及 UTF-8、UTF-16LE、UTF-16BE 文本。
    • 表与索引读取:多层 rowid 表、索引内部页记录、WITHOUT ROWID 表、overflow 链及 sqlite_schema。
    • 有界扫描:逐条回调、提前停止、完成状态,以及记录数、树深度、页数和 payload 预算。
    • 结构检查:单树检查和全局页归属报告,检测跨对象重复占页、freelist 冲突、未认领页及 auto-vacuum Ptrmap 不一致。
    • 宿主集成:Bytes 输入、可由外部实现的 PageSource 接口,以及 JSON CLI。

    #安装

    使用 MoonBit release 工具链,在现有项目中运行:

    moon add prowk/moonsqlitefile@0.3.0

    在消费包的 moon.pkg 中添加导入:

    import {
    "prowk/moonsqlitefile" @sqlite,
    }

    #快速上手

    宿主读取数据库文件后,将完整字节传入 open_database。以下示例读取普通 rowid 表的前 10 行:

    fn read_rows(data : Bytes) -> Array[@sqlite.Row] raise @sqlite.SqliteError {
    let db = @sqlite.open_database(data)
    db.table_rows("samples", limit=10)
    }

    Row 提供 rowid、values 和所在 page_number。值类型包括 Null、Integer(Int64)、Real(Double)、Text(String) 和 Blob(Bytes)。

    对于 WITHOUT ROWID 表或索引,使用原始记录 API:

    fn read_records(data : Bytes) -> Array[@sqlite.BTreeRecord] raise @sqlite.SqliteError {
    let db = @sqlite.open_database(data)
    db.table_records("keyed", limit=10)
    }

    fn read_index(data : Bytes) -> Array[@sqlite.BTreeRecord] raise @sqlite.SqliteError {
    let db = @sqlite.open_database(data)
    db.index_records("mixed_index", limit=10)
    }

    BTreeRecord 额外提供 cell_offset;索引和 WITHOUT ROWID 记录的 rowid 为 None。这两个示例对应仓库中的 fixtures/btree.sqlite。

    #逐条扫描

    用 scan_btree 消费记录,可避免在库中收集整个结果数组。根页号可从 db.schema() 的条目获取:

    fn scan_records(
    data : Bytes,
    root_page : Int,
    consume : (@sqlite.BTreeRecord) -> Bool raise @sqlite.SqliteError,
    ) -> @sqlite.ScanSummary raise @sqlite.SqliteError {
    let db = @sqlite.open_database(data)
    db.scan_btree(root_page, consume, limit=1000, max_total_payload_bytes=8388608UL)
    }

    回调返回 false 时停止扫描。检查 ScanSummary.completion,区分 Complete(已遍历全部记录)、RecordLimit(达到记录上限)和 VisitorStopped(回调主动停止)。结果还提供已读取的记录数、页数及累计 payload 字节数。

    #按需读取

    open_source(&PageSource) 接收宿主实现的数据源,初始化时仅读取文件头,后续按范围读取字节。接口同步返回 Bytes,数据源必须在整个读取期间保持同一份静态快照。接口定义和资源限额见 架构说明。

    #检查数据库结构

    fn inspect_file(data : Bytes) -> @sqlite.DatabaseInspection raise @sqlite.SqliteError {
    let db = @sqlite.open_database(data)
    db.inspect_database(max_total_payload_bytes=67108864UL, max_issues=100)
    }

    报告提供每页的用途、对象、根页和父页,以及冲突双方、未认领页和 Ptrmap 不一致等诊断。status 区分 Complete、Incomplete 和 Failed;ownership_complete 表示归属遍历完成,ptrmap_checked 表示反向指针检查完成或不适用,diagnostics_truncated 表示诊断已截断。达到资源限额时会保留已完成的结果;未完成根页发现时,不把未知页判定为孤儿。

    只检查一棵树时使用 db.inspect_btree(root_page);读取原始反向指针时使用 db.ptrmap_entries()。结构检查范围与各标记的含义详见 架构说明。

    #命令行工具

    CLI 需要 MoonBit release 工具链和 Node.js 22 或更新版本,无 npm 依赖:

    git clone https://github.com/prowk/MoonSQLiteFile.git cd MoonSQLiteFile moon build --target js cmd/inspect node tools/inspect.cjs fixtures/core.sqlite schema node tools/inspect.cjs fixtures/core.sqlite rows samples 10 node tools/inspect.cjs fixtures/btree.sqlite index mixed_index 5 node tools/inspect.cjs fixtures/btree.sqlite inspect

    将样本路径替换为自己的静态数据库副本即可检查实际文件。所有命令的格式为 node tools/inspect.cjs <file> <command> [arguments]:

    命令用途
    header读取数据库文件头
    schema列出 schema 条目及根页号
    page <number>检查指定 B-tree 页
    rows <table> [limit]读取普通 rowid 表
    records <table> [limit]读取普通表或 WITHOUT ROWID 表的原始记录
    index <name> [limit]读取索引原始记录
    scan <root> [limit]扫描指定根页,输出记录和扫描状态
    freelist检查空闲页链
    inspect输出全局页归属、Ptrmap 校验及结构化诊断

    成功时 stdout 输出一行 JSON;参数、初始化或读取错误写入 stderr,并返回非零退出码。inspect 能构造报告时始终向 stdout 输出 JSON:退出码 0 表示完整、1 表示失败、2 表示未完成,诊断包含在报告内。整数和 rowid 输出为十进制字符串,BLOB 输出为十六进制字符串,避免 JavaScript 丢失 64 位整数精度。rows 默认上限为 100000 行,显式传入 0 返回空数组。

    #支持范围与限制

    MoonSQLiteFile 返回磁盘存储值,字段保留磁盘顺序,不推断 SQL 列名、默认值或类型亲和性。普通表的 INTEGER PRIMARY KEY 字段通常存为 Null,其真实值位于 Row.rowid;WITHOUT ROWID 表按主键优先存储字段,索引记录可能附带 rowid 或主键字段。

    当前版本只读静态数据库文件,不执行 SQL、不写数据库、不合并 WAL。尚未验证索引排序与 collation、表与索引记录的一致性或完整 cell 空间覆盖,也不提供 SQL 列映射;同步 PageSource 使用 Int 偏移,尚未提供完整的大文件及异步 I/O 支持。

    扫描或检查结果为 Complete 只表示其覆盖范围内的工作完成,不等同于 SQLite integrity_check。解析错误通过 SqliteError 返回,分为 Invalid、Unsupported 和 LimitExceeded;检查 API 将错误保留在报告中。

    #开发与测试

    检出源码后,运行纯库示例:

    moon run --target js examples/basic

    预期输出:

    page_size=512, pages=1 schema entries=0

    完整验证需要 Python 3.13;SQLite 引擎仅用于样本生成和结果对照:

    moon check --target all --deny-warn moon build --target all --deny-warn moon test --target all --deny-warn moon fmt --check python tools/generate_fixtures.py --check python tools/verify_oracle.py python tools/generate_btree_fixtures.py --check python tools/verify_btree_oracle.py python tools/verify_inspection.py moon run --target js examples/basic python tools/verify_consumer.py

    v0.3.0 包含 55 项四后端用例、1063 行普通表数据与 4407 条索引及 WITHOUT ROWID 记录的 SQLite 对照,以及 192 次确定性字节变更回归扫描。页归属验证覆盖 13 个数据库、1765 页,包含 FULL / INCREMENTAL auto-vacuum、不同页尺寸与文本编码,以及受控的别名、孤儿页和 Ptrmap 损坏。CI 同时验证发布包可由独立项目消费。样本与生成方式见 fixtures 文档,API 变动见 版本记录。

    问题反馈或改进建议请提交到 GitHub Issues。报告解析问题时,请附上复现步骤、错误输出及可公开的最小数据库样本。

    #许可证

    采用 Apache-2.0 许可证。实现依据 SQLite 官方磁盘格式规范,未移植第三方解析器;详见 来源说明。

    PageSource

    pub(open) trait PageSource {
    fn byte_length(Self) -> Int
    fn read_range(Self, Int, Int) -> Bytes raise SqliteError
    }

    源必须在 Database 的使用期间保持同一份静态快照;不提供并发锁或事务语义。

    SqliteError

    pub(all) suberror SqliteError {
    Invalid(String)
    Unsupported(String)
    LimitExceeded(String)
    } derive(
    Debug
    )

    BTreeInspection

    pub(all) struct BTreeInspection {
    root_page : Int
    status : InspectionStatus
    records_decoded : Int
    summary : ScanSummary?
    error : SqliteError?
    } derive(
    Debug
    )

    不保存记录数组;发生错误时仍保留此前成功解码的记录数。

    BTreeRecord

    pub(all) struct BTreeRecord {
    rowid : Int64?
    values : Array[Value]
    page_number : Int
    cell_offset : Int
    } derive(
    Debug
    )

    原始存储记录;index B-tree 没有独立 rowid,字段按磁盘顺序保留。

    BytesSource

    pub struct BytesSource {
    data : Bytes
    }

    BytesSource::byte_length

    fn BytesSource::byte_length(self : BytesSource) -> Int

    BytesSource::new

    fn BytesSource::new(data : Bytes) -> BytesSource

    BytesSource::read_range

    fn BytesSource::read_range(self : BytesSource, offset : Int, count : Int) -> Bytes raise SqliteError

    Database

    pub struct Database {
    source : &PageSource
    header : Header
    page_count : Int
    limits : Limits
    }

    Database::freelist

    fn Database::freelist(self : Database) -> Array[Int] raise SqliteError

    返回 freelist 中的 trunk 与 leaf 页号,并校验计数与循环。

    Database::header

    fn Database::header(self : Database) -> Header

    Database::index_records

    fn Database::index_records(self : Database, name : String, limit? : Int) -> Array[BTreeRecord] raise SqliteError

    Database::inspect_btree

    fn Database::inspect_btree(self : Database, root : Int, limit? : Int, max_total_payload_bytes? : UInt64) -> BTreeInspection

    检查单棵 B-tree,保留完成状态或原始错误;资源不足不判为格式损坏。

    Database::inspect_database

    fn Database::inspect_database(self : Database, max_total_payload_bytes? : UInt64, max_issues? : Int) -> DatabaseInspection

    扫描 schema、独立 B-tree 和 freelist,使用整个检查过程共享的预算。

    Database::page

    fn Database::page(self : Database, number : Int) -> Page raise SqliteError

    Database::page_count

    fn Database::page_count(self : Database) -> Int

    Database::ptrmap_entries

    fn Database::ptrmap_entries(self : Database) -> Array[PtrmapEntry] raise SqliteError

    读取 auto-vacuum 反向指针;与真实页归属对照请使用 inspect_database。

    Database::read_btree

    fn Database::read_btree(self : Database, root : Int, limit? : Int, max_total_payload_bytes? : UInt64) -> Array[BTreeRecord] raise SqliteError

    Database::read_index

    fn Database::read_index(self : Database, root : Int, limit? : Int) -> Array[BTreeRecord] raise SqliteError

    Database::read_page

    fn Database::read_page(self : Database, number : Int) -> Bytes raise SqliteError

    Database::read_table

    fn Database::read_table(self : Database, root : Int, limit? : Int) -> Array[Row] raise SqliteError

    以 rowid 顺序读取普通表;保留首版接口与原始存储值语义。

    Database::scan_btree

    fn Database::scan_btree(self : Database, root : Int, visit : (BTreeRecord) -> Bool raise SqliteError, limit? : Int, max_total_payload_bytes? : UInt64) -> ScanSummary raise SqliteError

    按 B-tree 顺序逐条回调,返回 false 可停止;无需在库中保存所有结果。

    Database::schema

    fn Database::schema(self : Database) -> Array[SchemaEntry] raise SqliteError

    Database::table_records

    fn Database::table_records(self : Database, name : String, limit? : Int) -> Array[BTreeRecord] raise SqliteError

    返回普通表或 WITHOUT ROWID 表的原始存储记录,不推断 SQL 列顺序。

    Database::table_rows

    fn Database::table_rows(self : Database, name : String, limit? : Int) -> Array[Row] raise SqliteError

    DatabaseInspection

    pub(all) struct DatabaseInspection {
    status : InspectionStatus
    ownership_complete : Bool
    ptrmap_checked : Bool
    diagnostics_truncated : Bool
    roots_inspected : Int
    records_decoded : Int
    payload_bytes : UInt64
    pages : Array[PageOwnership]
    unclaimed_pages : Array[Int]
    issues : Array[DatabaseIssue]
    } derive(
    Debug
    )

    只检查逻辑数据库页;完整页归属不等价于 SQL 完整性检查。

    DatabaseIssue

    pub(all) enum DatabaseIssue {
    ScanError(Int?, SqliteError)
    PageConflict(PageOwnership, PageOwnership)
    UnclaimedPage(Int)
    UnsupportedFeature(String)
    PtrmapMismatch(PtrmapEntry, PageOwnership)
    } derive(
    Debug
    )
    pub(all) struct Header {
    page_size : Int
    usable_size : Int
    write_version : Int
    read_version : Int
    change_counter : UInt
    declared_pages : UInt
    freelist_trunk : UInt
    freelist_pages : UInt
    schema_cookie : UInt
    schema_format : Int
    largest_root_page : UInt
    incremental_vacuum : Bool
    text_encoding : Int
    user_version : UInt
    application_id : UInt
    version_valid_for : UInt
    sqlite_version : UInt
    } derive(Eq,
    Debug
    )

    Header::equal

    fn Header::equal(Header, Header) -> Bool

    Header::not_equal

    fn Header::not_equal(x : Header, y : Header) -> Bool

    Header::to_repr

    InspectionStatus

    pub(all) enum InspectionStatus {
    Complete
    Incomplete
    Failed
    } derive(Eq,
    Debug
    )

    仅描述指定根页的扫描结果,不代表整个数据库的完整性结论。

    InspectionStatus::equal

    InspectionStatus::not_equal

    fn InspectionStatus::not_equal(x : InspectionStatus, y : InspectionStatus) -> Bool

    Limits

    pub(all) struct Limits {
    max_rows : Int
    max_payload_bytes : Int
    max_pages : Int
    max_depth : Int
    } derive(
    Debug
    )

    Limits::default

    fn Limits::default() -> Limits

    Limits::to_repr

    Page

    pub(all) struct Page {
    number : Int
    kind : PageType
    first_freeblock : Int
    cell_count : Int
    content_start : Int
    fragmented_bytes : Int
    right_child : Int?
    cell_offsets : Array[Int]
    } derive(
    Debug
    )

    Page::to_repr

    PageKind

    pub(all) enum PageKind {
    BtreeRoot
    BtreeChild
    OverflowFirst
    OverflowContinuation
    FreelistTrunk
    FreelistLeaf
    PointerMap
    LockByte
    } derive(Eq,
    Debug
    )

    PageKind::equal

    fn PageKind::equal(PageKind, PageKind) -> Bool

    PageKind::not_equal

    fn PageKind::not_equal(x : PageKind, y : PageKind) -> Bool

    PageKind::to_repr

    PageOwnership

    pub(all) struct PageOwnership {
    page_number : Int
    kind : PageKind
    root_page : Int?
    object_name : String?
    parent_page : Int?
    } derive(Eq,
    Debug
    )

    PageOwnership::equal

    PageOwnership::not_equal

    fn PageOwnership::not_equal(x : PageOwnership, y : PageOwnership) -> Bool

    PageType

    pub(all) enum PageType {
    TableLeaf
    TableInterior
    IndexLeaf
    IndexInterior
    } derive(Eq,
    Debug
    )

    PageType::equal

    fn PageType::equal(PageType, PageType) -> Bool

    PageType::not_equal

    fn PageType::not_equal(x : PageType, y : PageType) -> Bool

    PageType::to_repr

    PtrmapEntry

    pub(all) struct PtrmapEntry {
    page_number : Int
    map_page : Int
    kind : PtrmapType
    parent_page : Int
    } derive(Eq,
    Debug
    )

    PtrmapEntry::equal

    fn PtrmapEntry::equal(PtrmapEntry, PtrmapEntry) -> Bool

    PtrmapEntry::not_equal

    fn PtrmapEntry::not_equal(x : PtrmapEntry, y : PtrmapEntry) -> Bool

    PtrmapType

    pub(all) enum PtrmapType {
    RootPage
    FreePage
    FirstOverflow
    NextOverflow
    NonRootBtree
    } derive(Eq,
    Debug
    )

    PtrmapType::equal

    fn PtrmapType::equal(PtrmapType, PtrmapType) -> Bool

    PtrmapType::not_equal

    fn PtrmapType::not_equal(x : PtrmapType, y : PtrmapType) -> Bool

    Row

    pub(all) struct Row {
    rowid : Int64
    values : Array[Value]
    page_number : Int
    } derive(
    Debug
    )

    Row::to_repr

    ScanCompletion

    pub(all) enum ScanCompletion {
    Complete
    RecordLimit
    VisitorStopped
    } derive(Eq,
    Debug
    )

    ScanCompletion::equal

    ScanCompletion::not_equal

    fn ScanCompletion::not_equal(x : ScanCompletion, y : ScanCompletion) -> Bool

    ScanSummary

    pub(all) struct ScanSummary {
    records_read : Int
    pages_read : Int
    payload_bytes : UInt64
    completion : ScanCompletion
    } derive(
    Debug
    )

    SchemaEntry

    pub(all) struct SchemaEntry {
    object_type : String
    name : String
    table_name : String
    root_page : Int
    sql : String?
    } derive(
    Debug
    )

    Value

    pub(all) enum Value {
    Null
    Integer(Int64)
    Real(Double)
    Text(String)
    Blob(Bytes)
    } derive(Eq,
    Debug
    )

    SQLite 记录中的原始值,保留整数、浮点数与二进制数据的区别。

    Value::equal

    fn Value::equal(Value, Value) -> Bool

    Value::not_equal

    fn Value::not_equal(x : Value, y : Value) -> Bool

    Value::to_repr

    decode_record

    fn decode_record(payload : Bytes, encoding : Int) -> Array[Value] raise SqliteError

    解码完整的 SQLite record payload;编码编号与数据库文件头保持一致。

    decode_varint

    fn decode_varint(data : Bytes, offset : Int) -> (UInt64, Int) raise SqliteError

    open_database

    fn open_database(data : Bytes, limits? : Limits) -> Database raise SqliteError

    open_source

    fn open_source(source : &PageSource, limits? : Limits) -> Database raise SqliteError

    parse_header

    fn parse_header(data : Bytes) -> Header raise SqliteError