moonrdb

    A native MoonBit Redis RDB parser and offline snapshot analysis CLI (under development).

    redis
    rdb
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    6 hours ago
    Downloads
    2

    No dependencies

    #MoonRDB

    计划使用 MoonBit 实现 Redis RDB 解析库与离线快照分析 CLI。

    #当前状态

    已完成 F0 工程骨架、F1 基础读取、F2 空库解析、F3 String 与 LZF、F4 Hash、F5 List,F6 Set 三种表示,F7 Sorted Set 两种表示解码,F8 inspect CLI 与最小库示例、F9 stats、F10 TopN 和 F11 JSONL 导出。 公开 API read_header 只读取头部,不验证剩余文件内容、EOF 或 CRC64。 公开 API parse_empty 可完整检查支持范围内的无对象快照,保留 AUX、数据库选择和容量提示,并区分校验通过与关闭校验和。 parse_strings 保留只接受 String 的边界。通用 parse 返回 Snapshot,其 RdbRecord 可为 String、Hash、List、Set 或 SortedSet。Set 支持类型 2 普通成员序列、11 intset 和 20 listpack;检查成员唯一性与编码完整性,受全文件累计成员数限制。 Sorted Set 支持类型 5 二进制 double 与 17 listpack,返回原始成员和 Double 分数,拒绝 NaN、重复成员与损坏结构。inspect 已可检查格式版本、数据库、键数和完整性;stats 已支持数据库/类型/前缀体积汇总、指定时刻的过期状态和 TTL 分布;top 已支持按序列化体积排名,显示类型、编码和元素数量;export 已支持数据库/前缀过滤和 JSONL 输出,保留二进制与宽整数精度。

    #本地开发

    已验证 Windows x64 native 后端:moonc 0.10.18+6c83a7f62,moon 0.1.20261028。native 编译需要可用的 MSVC 工具链,样本脚本需要 Python 3.10 或以上。本阶段没有第三方 MoonBit 包依赖。

    在仓库根目录运行:

    moon check --target native moon build --target native moon test --target native moon run cmd/main --target native -- --help moon run cmd/main --target native -- inspect fixtures/redis-7.2.5-sorted-sets/sorted-sets.rdb moon run examples/parse --target native python -X utf8 scripts/verify_fixtures.py

    当前 moon test --target native 为 155 项测试(含 7 项文档测试),覆盖七份真实快照及合成异常输入。有键快照覆盖 String、Hash、List、Set、Sorted Set,包含数据库切换、LRU / LFU 和到期时间,并经隔离 Redis 重新加载逐键核对。Set 样本覆盖三种表示及所有 intset 宽度;新增 Sorted Set 样本含 6 个 Sorted Set 与两个混合类型键,覆盖两种表示及分数边界。List 的 plain 节点使用官方 DEBUG 命令降低阈值生成,不代表默认阈值下的大数据验收。旧秒编码仍只有合成测试。文件读取放在 native CLI 与测试中,库本身不依赖文件系统。 Python 离线检查核对七份样本及预期数据的身份,完整解析与 CRC64 由 MoonBit 测试验证。另有 85 项原生 CLI 进程检查,覆盖退出码、标准输出/错误流、异常文件、路径和输入上限。

    #inspect 用法

    moon run cmd/main --target native -- inspect fixtures/redis-7.2.5/empty.rdb --max-input-bytes 67108864

    默认只读普通文件,输入上限为 64 MiB,在分配完整输入前检查文件大小。成功摘要最后显示 status: complete;CRC64 有效与关闭校验和分别报告。原生程序退出码:成功 0,读取/解析失败 1,参数错误 2。完整命令、输出示例与限制见 inspect 文档。

    #stats 用法

    moon run cmd/main --target native -- stats fixtures/redis-7.2.5-sorted-sets/sorted-sets.rdb --at 2100-01-01T00:00:00.123Z --prefix long-

    不指定 --at 时不按当前时间判断过期。前缀汇总不改变全文件统计,体积为 RDB 序列化字节,不是运行时内存。时间格式、TTL 档位与前缀规则见 stats 文档。

    #大键 TopN

    moon run cmd/main --target native -- top fixtures/redis-7.2.5-sorted-sets/sorted-sets.rdb --by serialized-bytes --limit 20

    默认返回体积最大的 20 条记录,键以 Base64 展示;同体积排序固定,候选最多 N 条。完整参数、排序与资源边界见 top 文档。

    #JSONL 导出

    moon run cmd/main --target native -- export fixtures/redis-7.2.5-sorted-sets/sorted-sets.rdb --db 2 --prefix long- --format jsonl --output snapshot.jsonl

    不指定 output 时 stdout 只含 JSONL;完成状态写 stderr。二进制统一 Base64,宽整数写字符串,Sorted Set 保留 IEEE 分数位模式。文件先写同目录临时文件,完整成功后替换目标。详见 导出约定。另有 39 项导出进程检查验证还原和文件失败保护。

    #库调用示例

    消费项目的 moon.pkg 导入 "Noverberrain/moonrdb" 后,可以调用:

    let version = @moonrdb.read_header(b"REDIS0011") // 返回 11;调用方需要处理 ParseError
    let result = @moonrdb.parse_empty(bytes) // bytes 是调用方读取的快照;可能抛出 ParseError
    let strings = @moonrdb.parse_strings(bytes) // StringSnapshot,含 records 与校验状态
    let snapshot = @moonrdb.parse(bytes) // Snapshot,records 中的 String / Hash / List / Set / SortedSet 可模式匹配

    所有 API 均接收完整 Bytes,默认限制输入为 64 MiB,可通过 max_input_bytes 调整;限制不会撤销调用方已分配的输入。仅支持格式版本 11,其他版本返回明确错误。读取约定、空库约定、String 约定、Hash 约定、List 约定、Set 约定 和 Sorted Set 约定 说明具体边界。当前 API 会保留所有解码记录,不是恒定内存的分块文件读取。

    可运行 moon run examples/parse --target native 验证只依赖解析库的最小示例。它在仓库内构建,独立消费项目验证留待 F12。

    完整本地检查可执行 ./scripts/ci.ps1。GitHub Actions 固定编译器、core 和第三方 Action 提交,运行 native check / build / test、格式检查、样本核验和 CLI 进程检查;远端运行结果以实际 Actions 记录为准。

    #格式与样本

    参考 Redis 7.2.5,固定上游提交 f60370ce28b946c1146dcea77c9c399d39601aaa,对应 RDB 格式版本 11。当前支持明确列出的文件结构、String、Hash、quicklist2 List、Set 和 Sorted Set,不代表支持该产品版本的所有数据类型。

    #计划范围

    • 固定 Redis 7.2 的具体补丁版本及上游提交,逐项验证 RDB 编码。
    • 提供可复用解析 API,以及 inspect、stats、top 和 export 命令。
    • 统计快照中的序列化体积;该数值不代表 Redis 运行时内存占用。

    下一阶段为 F12:独立消费者、发布材料和验收演示。当前版本尚未发布到 Mooncakes;上述导入示例需先使用本地源码,不能直接从 Mooncakes 安装本项目。

    ParseError

    pub(all) suberror ParseError {
    InvalidInputLimit(offset~ : Int, limit~ : Int)
    InputTooLarge(offset~ : Int, size~ : Int, limit~ : Int)
    UnexpectedEnd(offset~ : Int, needed~ : UInt64, remaining~ : Int)
    InvalidMagic(offset~ : Int)
    InvalidVersionDigit(offset~ : Int)
    UnsupportedVersion(offset~ : Int, version~ : Int)
    InvalidLengthTag(offset~ : Int, tag~ : Byte)
    InvalidStringEncoding(offset~ : Int, encoding~ : Int)
    UnexpectedEncodedLength(offset~ : Int)
    InvalidResourceLimit(offset~ : Int, resource~ : String, limit~ : Int)
    ResourceLimitExceeded(offset~ : Int, resource~ : String, limit~ : UInt64, actual~ : UInt64)
    UnsupportedStringEncoding(offset~ : Int, encoding~ : Int)
    UnsupportedObjectType(offset~ : Int, object_type~ : Byte)
    UnsupportedOpcode(offset~ : Int, opcode~ : Byte)
    DuplicateKeyMetadata(offset~ : Int, opcode~ : Byte)
    DanglingKeyMetadata(offset~ : Int)
    ChecksumMismatch(offset~ : Int, stored~ : UInt64, computed~ : UInt64)
    TrailingData(offset~ : Int, remaining~ : Int)
    InvalidLzf(offset~ : Int, reason~ : String)
    InvalidHash(offset~ : Int, reason~ : String)
    InvalidListpack(offset~ : Int, inner_offset~ : Int, reason~ : String)
    InvalidList(offset~ : Int, reason~ : String)
    InvalidSortedSet(offset~ : Int, reason~ : String)
    InvalidSet(offset~ : Int, reason~ : String)
    InvalidIntset(offset~ : Int, inner_offset~ : Int, reason~ : String)
    } derive(
    Debug
    )

    Offsets are zero-based byte offsets in the supplied input.

    AuxField

    pub(all) struct AuxField {
    key : Bytes
    value : Bytes
    } derive(Eq,
    Debug
    )

    Raw binary AUX key and value. Integer-encoded values are decimal bytes.

    AuxField::equal

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

    AuxField::not_equal

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

    AuxField::to_repr

    ChecksumStatus

    pub(all) enum ChecksumStatus {
    Valid(UInt64)
    Disabled
    } derive(Eq,
    Debug
    )

    A zero stored checksum means disabled, never validated.

    ChecksumStatus::equal

    ChecksumStatus::not_equal

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

    DatabaseSizeHint

    pub(all) struct DatabaseSizeHint {
    database : UInt64
    keys : UInt64
    expires : UInt64
    } derive(Eq,
    Debug
    )

    RESIZEDB values are allocation hints, not measured key counts.

    DatabaseSizeHint::equal

    DatabaseSizeHint::not_equal

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

    EmptySnapshot

    pub(all) struct EmptySnapshot {
    format_version : Int
    aux_fields : Array[AuxField]
    database_selections : Array[UInt64]
    resize_hints : Array[DatabaseSizeHint]
    checksum : ChecksumStatus
    file_size : Int
    } derive(Eq,
    Debug
    )

    A complete version-11 file containing no objects. Explicit selections and hints retain input order, including duplicates. No implicit DB is listed.

    EmptySnapshot::equal

    EmptySnapshot::not_equal

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

    HashEntry

    pub(all) struct HashEntry {
    field : Bytes
    value : Bytes
    } derive(Eq,
    Debug
    )

    HashEntry::equal

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

    HashEntry::not_equal

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

    HashRecord

    pub(all) struct HashRecord {
    database : UInt64
    key : Bytes
    key_storage : StringStorage
    entries : Array[HashEntry]
    storage : HashStorage
    packed_storage : StringStorage?
    expires_at_ms : Int64?
    idle_seconds : UInt64?
    frequency : Byte?
    record_start : Int
    record_end : Int
    serialized_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Fields retain wire order, not a semantic ordering of the Hash. packed_storage describes the outer RDB string only for listpack objects.

    HashRecord::equal

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

    HashRecord::not_equal

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

    HashStorage

    pub(all) enum HashStorage {
    Hashtable
    Listpack
    } derive(Eq,
    Debug
    )

    HashStorage::equal

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

    HashStorage::not_equal

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

    ListNode

    pub(all) struct ListNode {
    container : ListNodeContainer
    storage : StringStorage
    item_start : Int
    item_end : Int
    node_start : Int
    node_end : Int
    } derive(Eq,
    Debug
    )

    Node ranges refer to the original file and include container and RDB string. Item ranges index ListRecord.items; no decoded payload is duplicated here.

    ListNode::equal

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

    ListNode::not_equal

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

    ListNode::to_repr

    ListNodeContainer

    pub(all) enum ListNodeContainer {
    Plain
    Packed
    } derive(Eq,
    Debug
    )

    ListNodeContainer::equal

    ListNodeContainer::not_equal

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

    ListRecord

    pub(all) struct ListRecord {
    database : UInt64
    key : Bytes
    key_storage : StringStorage
    items : Array[Bytes]
    nodes : Array[ListNode]
    expires_at_ms : Int64?
    idle_seconds : UInt64?
    frequency : Byte?
    record_start : Int
    record_end : Int
    serialized_bytes : UInt64
    } derive(Eq,
    Debug
    )

    RDB type 18 (quicklist2). Ordered binary items, including duplicates and empty packed entries. Legacy List encodings are not supported.

    ListRecord::equal

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

    ListRecord::not_equal

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

    RdbRecord

    pub(all) enum RdbRecord {
    String(StringRecord)
    Hash(HashRecord)
    List(ListRecord)
    Set(SetRecord)
    SortedSet(SortedSetRecord)
    } derive(Eq,
    Debug
    )

    RdbRecord::equal

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

    RdbRecord::not_equal

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

    SetRecord

    pub(all) struct SetRecord {
    database : UInt64
    key : Bytes
    key_storage : StringStorage
    members : Array[Bytes]
    storage : SetStorage
    packed_storage : StringStorage?
    expires_at_ms : Int64?
    idle_seconds : UInt64?
    frequency : Byte?
    record_start : Int
    record_end : Int
    serialized_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Members retain input order, not semantic Set ordering. Intset storage records the element width in bytes (2, 4 or 8). Packed storage describes the outer RDB string for intset/listpack only. Binary values are never UTF-8 replaced.

    SetRecord::equal

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

    SetRecord::not_equal

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

    SetStorage

    pub(all) enum SetStorage {
    Hashtable
    Intset(Int)
    Listpack
    } derive(Eq,
    Debug
    )

    SetStorage::equal

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

    SetStorage::not_equal

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

    Snapshot

    pub(all) struct Snapshot {
    format_version : Int
    aux_fields : Array[AuxField]
    database_selections : Array[UInt64]
    resize_hints : Array[DatabaseSizeHint]
    records : Array[RdbRecord]
    checksum : ChecksumStatus
    file_size : Int
    global_overhead_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Complete snapshot restricted to the explicitly supported object encodings.

    Snapshot::equal

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

    Snapshot::not_equal

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

    Snapshot::to_repr

    SortedSetEntry

    pub(all) struct SortedSetEntry {
    element : Bytes
    score : Double
    } derive(Eq,
    Debug
    )

    SortedSetEntry::equal

    SortedSetEntry::not_equal

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

    SortedSetRecord

    pub(all) struct SortedSetRecord {
    database : UInt64
    key : Bytes
    key_storage : StringStorage
    entries : Array[SortedSetEntry]
    storage : SortedSetStorage
    packed_storage : StringStorage?
    expires_at_ms : Int64?
    idle_seconds : UInt64?
    frequency : Byte?
    record_start : Int
    record_end : Int
    serialized_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Entries retain wire order, which is not guaranteed to be score order.

    SortedSetRecord::equal

    SortedSetRecord::not_equal

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

    SortedSetStorage

    pub(all) enum SortedSetStorage {
    BinaryDouble
    Listpack
    } derive(Eq,
    Debug
    )

    SortedSetStorage::equal

    SortedSetStorage::not_equal

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

    StringRecord

    pub(all) struct StringRecord {
    database : UInt64
    key : Bytes
    value : Bytes
    key_storage : StringStorage
    value_storage : StringStorage
    expires_at_ms : Int64?
    idle_seconds : UInt64?
    frequency : Byte?
    record_start : Int
    record_end : Int
    serialized_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Binary-safe String record. The half-open range includes key metadata, object type, key and value as serialized, including compressed bytes.

    StringRecord::equal

    StringRecord::not_equal

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

    StringSnapshot

    pub(all) struct StringSnapshot {
    format_version : Int
    aux_fields : Array[AuxField]
    database_selections : Array[UInt64]
    resize_hints : Array[DatabaseSizeHint]
    records : Array[StringRecord]
    checksum : ChecksumStatus
    file_size : Int
    global_overhead_bytes : UInt64
    } derive(Eq,
    Debug
    )

    Complete snapshot restricted to String objects. Records preserve file order; no expiry filtering, duplicate-key replacement or clock-dependent behavior.

    StringSnapshot::equal

    StringSnapshot::not_equal

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

    StringStorage

    pub(all) enum StringStorage {
    Raw
    Integer8
    Integer16
    Integer32
    Lzf
    } derive(Eq,
    Debug
    )

    The actual string representation in the input, independently for key/value.

    StringStorage::equal

    StringStorage::not_equal

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

    parse

    fn parse(input : Bytes, max_input_bytes? : Int, max_string_bytes? : Int, max_metadata_entries? : Int, max_records? : Int, max_decoded_bytes? : Int, max_hash_entries? : Int, max_packed_bytes? : Int, max_list_nodes? : Int, max_list_items? : Int, max_set_members? : Int, max_sorted_set_entries? : Int) -> Snapshot raise ParseError

    Parse a complete version-11 snapshot of supported String, Hash, List, Set and Sorted Set. Packed container scratch space is bounded separately from returned data. List node and item limits apply to the whole snapshot; empty items count. Set member and Sorted Set entry limits apply across the whole snapshot. Decoded bytes count member bytes; fixed-size scores and score scratch do not.

    test {
    let result = @moonrdb.parse(
    b"REDIS0011\x04\x01k\x01\x01f\x01v\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    assert_eq(result.records.length(), 1)
    }

    test {
    let result = @moonrdb.parse(
    b"REDIS0011\x12\x00\x01\x01\x01v\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    guard result.records[0] is List(record) else { fail("expected List") }
    assert_eq(record.items, [b"v"])
    }

    test {
    let result = @moonrdb.parse(
    b"REDIS0011\x02\x00\x01\x01v\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    guard result.records[0] is Set(record) else { fail("expected Set") }
    assert_eq(record.members, [b"v"])
    }

    test {
    let result = @moonrdb.parse(
    b"REDIS0011\x05\x00\x01\x01v\x00\x00\x00\x00\x00\x00\xf0\x3f\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    guard result.records[0] is SortedSet(record) else {
    fail("expected Sorted Set")
    }
    assert_eq(record.entries[0].score, 1.0)
    }

    parse_empty

    fn parse_empty(input : Bytes, max_input_bytes? : Int, max_string_bytes? : Int, max_metadata_entries? : Int) -> EmptySnapshot raise ParseError

    Parse a complete, object-free format-11 snapshot, including CRC64. AUX LZF strings, objects, functions and modules are not supported yet. Arrays preserve file order; RESIZEDB numbers are only hints.

    test {
    let result = @moonrdb.parse_empty(
    b"REDIS0011\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    assert_true(result.checksum is Disabled)
    }

    parse_strings

    fn parse_strings(input : Bytes, max_input_bytes? : Int, max_string_bytes? : Int, max_metadata_entries? : Int, max_records? : Int, max_decoded_bytes? : Int) -> StringSnapshot raise ParseError

    Parse a complete format-11 snapshot containing only String objects. Retains all decoded records and absolute expiration times, including expired keys. The decoded-byte budget counts AUX, keys and values before allocation.

    test {
    let result = @moonrdb.parse_strings(
    b"REDIS0011\x00\x01k\x01v\xff\x00\x00\x00\x00\x00\x00\x00\x00",
    )
    assert_eq(result.records[0].value, b"v")
    }

    read_header

    fn read_header(input : Bytes, max_input_bytes? : Int) -> Int raise ParseError

    Read only the nine-byte RDB header, accepting format version 11. The rest of the file is not parsed or checked. The limit applies to the entire supplied input and does not undo its allocation by the caller.

    test {
    assert_eq(@moonrdb.read_header(b"REDIS0011"), 11)
    }

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io