orderkey

    Order-preserving compound keys and typed range bounds for ordered byte stores.

    ordered-key
    storage
    key-value
    Download zip
    Author
    Version
    0.0.8
    License
    MIT
    Last updated
    2 hours ago
    Downloads
    9

    No dependencies

    #OrderKey

    OrderKey 是一个 MoonBit 库,给按无符号字节字典序排序的存储引擎生成复合键和扫描区间。调用方固定索引的字段类型与升降序后,可以按前导字段查找、在下一字段限定区间,或从上一页末尾继续扫描。

    小版本的交付见 CHANGELOG.md,本版的本地验证见 v0.0.4 验证记录。包版本与 v1 键格式是独立的。

    #复现

    在完整项目的根目录运行。完整源码见 GitHub。此版本使用 MoonBit v0.10.20 工具链验证,MoonLSM 演示的 JS 后端需要 Node.js。 MoonBit 工具链下载

    moon update moon test --target wasm . moon test --target wasm-gc . moon test --target js . moon run --target js examples/moonlsm

    可选的独立验证需要 Python 3.9+。脚本对合成数据和项目内的 NOAA 原始观测各运行一次双引擎验证,把 JS 和 Wasm GC 的键逐字节核对,再用 SQLite 比较多列排序、BLOB 排序和时间区间结果。

    python examples/moonlsm/verify_sqlite.py

    在有本地 v0.0.1 Git 标签的工作区,可以加 --baseline 从干净目录重建 v0.0.1 和 v0.0.4,并核对两个版本的键字节。

    核心库只依赖 MoonBit 标准库。工作区的演示使用 MoonLSM 0.3.2 和 MoonFST 0.1.1。

    #使用示例

    从 Mooncakes 安装当前公开版本。

    moon add WeiR-h/orderkey@0.0.8

    在调用方的 moon.pkg 中导入 "WeiR-h/orderkey"。下面的函数生成一条记录的键,以及同一租户和设备上时间戳 [-2, 4) 的扫描范围。

    fn make_query() -> (Bytes, @orderkey.ByteRange) raise @orderkey.KeyError {
    let spec = @orderkey.KeySpec::new([
    BytesField(Asc), BytesField(Asc), Int64Field(Desc), Int64Field(Asc),
    ])
    let key = spec.encode([Binary(b"t3"), Binary(b"pump"), Signed(0L), Signed(1L)])
    let range = spec.field_range(
    [Binary(b"t3"), Binary(b"pump")],
    Inclusive(Signed(-2L)),
    Exclusive(Signed(4L)),
    )
    (key, range)
    }

    • encode 和 decode 处理完整的键。字段索引与解码错误偏移均从 0 开始。
    • prefix_range 按完整的前导字段匹配。设备 pump 与 pump\x00A 是不同的字段值。
    • field_range 先限定前导字段,再给紧接着的一个字段指定范围。输入端点始终按值的自然升序表达,Desc 字段会由库转换为相应的字节顺序。
    • tuple_range 接受由多个后续字段组成的端点,端点按配置的索引顺序给出。端点可以只给出部分字段,包含或排除作用于与其共享前缀的整组键。
    • layout 生成持久化布局描述符,from_layout 可恢复布局,require_layout 用于重新打开索引时检查已有布局和调用方预期是否一致。
    • Span(start, Some(end)) 表示 [start, end),Span(start, None) 表示上界无穷,Empty 表示空区间。范围语义针对按同一布局写入的完整键。
    • resume_after 检查游标是否是范围内的完整键,并排除该键。需要并发写入期间稳定翻页时,存储层须提供一致的读快照。

    #离线换序与验收

    在有序 KV 上自建索引时,保存 IndexContract 描述符。它包含稳定的索引 ID、代次和按顺序排列的字段 ID。交换两个升序字节字段时,原有布局的类型标签一样,新的契约能识别字段含义变化。旧索引的字段 ID 由应用显式提供。

    • 用 IndexContract::new 建立源和目标契约。打开索引时通过 require_same 验证保存的描述符。
    • 调用 ReindexPlan::new 预检重排与方向变更,然后对冻结记录调用 rekey,把新键与原载荷写入独立目标。目标代次递增,字段 ID 及其类型对应一致。
    • 续读时将原始区间、存储层提供的稳定快照 ID 和实际末键传入 issue_cursor。resume_cursor 检查同一索引、代次、快照和区间。公开接口的游标应由应用层鉴别完整性。
    • 重开并核对目标后,由调用方决定何时切换。示例使用 MoonLSM 的 MemStorage 重开 SSTable,并从目标记录生成、重载 MoonFST,只读索引保存目标记录序号。

    在完整源码的根目录,用 Python 3.10+、Node.js 与 MoonBit 运行。

    moon update python examples/moonlsm/verify_reindex.py --report reindex-report.json

    报告逐项记录源与目标契约、行数、重复数、源前后 SHA-256、查询结果 SHA-256 和首个差异。源摘要覆盖引擎扫描出的键值载荷。ready 表示此轮复验的目标可作为切换候选。负向检查覆盖错误布局、旧游标、坏键、目标重复和存储写入失败。SQLite 按原始类型和值独立验证合成遥测和 NOAA 观测。

    #v1 键格式

    字段升序编码降序编码
    Int64翻转符号位,写成 8 字节大端序逐字节反转升序编码的所有位
    Bytes00 转义成 00 FF,字段以 00 00 结束逐字节反转升序编码的所有位

    完整键按布局顺序连接各字段。字节字段用原始字节比较,空字节串和包含 00、FF 的值均可编码。索引元数据应保存版本与字段布局。布局变化时,为新布局重建索引,避免在一个字节排序空间内混用不同布局。

    布局描述符以 4F 4B 01 开头,随后每个字段占一个字节。字节串升序为 10,字节串降序为 11,整数升序为 20,整数降序为 21。描述符存到索引的元数据位置,原有数据键沿用 v1 编码。

    #双引擎演示

    示例写入 9,840 条模拟设备读数,刷新 SSTable 后重新打开数据库。固定查询跨越负数和零,同一时间戳包含三条记录。

    • 比较全扫后过滤、手写正确字节边界和 OrderKey 生成边界的逐条结果。
    • 对同一查询做游标分页,并与单次范围查询核对顺序与记录数量。
    • 重新打开后检查持久化布局,再用时间戳和序号共同限定区间,验证边界可以切开同一时间戳的记录。
    • 将相同键和原始行的序号构建成 MoonFST 只读索引,序列化并重新载入。按设备取前缀、复合区间和游标续读的结果都与 MoonLSM 对照。
    • 统计扫描返回给调用方的行数,以及键和值的字节总数。示例用 MoonLSM 自带的内存存储,SSTable 在打开时载入。返回的数据量和物理磁盘读取量是两个指标。

    扫描路径引擎返回行数返回的键和值
    全扫后由调用方过滤9,840334,560 字节
    手写正确边界18648 字节
    OrderKey 生成边界18648 字节

    三条路径最终得到相同的 18 行。游标分为 5、5、5、3 条,拼接结果与单次查询一致。示例的 MoonLSM 适配代码使用有限的上界,核心库同时支持上界无穷。

    MoonFST 快照只包含键和记录序号,真实键值内容仍由 MoonLSM 返回。SQLite 独立核对两个引擎的返回键和顺序。

    #公开数据样本

    样本来自 NOAA NCEI 的 GHCNd Daily Summaries。原始 CSV 包含三个美国测站在 2024 年第一季度的 273 条观测,保留 STATION、DATE、TMAX、TMIN 四列。脚本先检查原始字节的 SHA-256,再把日期按 YYYYMMDD 写入降序索引,温度作为原样文本值写入 MoonLSM。 原始 CSV · NOAA 数据接口 · NCEI 开放数据政策

    对 USW00094846 查询日期范围 [20240201, 20240301),两个引擎各返回 29 条,从 2024 年 2 月 29 日开始降序排列。复现使用项目内的原始文件。

    设计参考可比较键的公开先例 FoundationDB Tuple Layer 和 Google orderedcode。OrderKey 使用上表中的 v1 字节格式。

    ContractError

    pub suberror ContractError {
    InvalidContract
    ContractMismatch
    InvalidCursor
    CursorScopeMismatch
    } derive(
    Debug
    )

    Failures when reading or comparing index and cursor metadata.

    KeyError

    pub suberror KeyError {
    EmptySpec
    WrongArity(Int, Int)
    InvalidPrefixLength(Int)
    TypeMismatch(Int)
    TruncatedKey(Int)
    InvalidEscape(Int)
    TrailingBytes(Int)
    CursorOutsideRange
    InvalidLayout(Int)
    UnsupportedKeyFormat(Byte)
    LayoutMismatch
    InvalidTupleLength(Int)
    } derive(
    Debug
    )

    Field indexes and byte offsets in errors are zero-based.

    KeyError::to_repr

    ReindexError

    pub suberror ReindexError {
    DifferentIndex
    InvalidGeneration
    IncompatibleFields
    } derive(
    Debug
    )

    Bound

    pub(all) enum Bound {
    Unbounded
    Inclusive(Value)
    Exclusive(Value)
    }

    A bound on the next field in its logical, ascending value order.

    ByteRange

    pub(all) enum ByteRange {
    Empty
    Span(Bytes, Bytes?)
    } derive(Eq,
    Debug
    )

    A half-open byte range. A missing upper bound means positive infinity.

    ByteRange::contains

    fn ByteRange::contains(self : ByteRange, key : Bytes) -> Bool

    ByteRange::equal

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

    ByteRange::not_equal

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

    Direction

    pub(all) enum Direction {
    Asc
    Desc
    }

    Field

    pub(all) enum Field {
    BytesField(Direction)
    Int64Field(Direction)
    }

    IndexContract

    pub struct IndexContract {
    // private fields
    }

    Logical identity and physical layout for one generation of an index. Field identifiers are application-assigned, nonempty byte strings.

    IndexContract::from_bytes

    fn IndexContract::from_bytes(input : Bytes) -> IndexContract raise ContractError

    IndexContract::issue_cursor

    fn IndexContract::issue_cursor(self : IndexContract, snapshot_id : Bytes, range : ByteRange, last_key : Bytes) -> Bytes raise

    Issues a cursor for the last actual key of a page in the original range. Use a stable snapshot ID supplied by the storage layer. This is a scope check for trusted cursors, so applications exposing tokens authenticate them.

    IndexContract::key_spec

    fn IndexContract::key_spec(self : IndexContract) -> KeySpec

    IndexContract::new

    fn IndexContract::new(index_id : Bytes, generation : UInt64, fields : Array[(Bytes, Field)]) -> IndexContract raise ContractError

    IndexContract::require_same

    fn IndexContract::require_same(self : IndexContract, persisted : Bytes) -> Unit raise ContractError

    IndexContract::resume_cursor

    fn IndexContract::resume_cursor(self : IndexContract, snapshot_id : Bytes, range : ByteRange, token : Bytes) -> ByteRange raise

    Resumes strictly after the saved key, using the same original range.

    IndexContract::to_bytes

    fn IndexContract::to_bytes(self : IndexContract) -> Bytes

    Canonical v1 metadata: OKC 01, generation (u64 BE), then u32-length byte strings for index ID, v1 key layout, and each ordered field ID.

    KeySpec

    pub struct KeySpec {
    // private fields
    }

    A fixed layout for one ordered index. Keys carry no schema metadata.

    KeySpec::decode

    fn KeySpec::decode(self : KeySpec, key : Bytes) -> Array[Value] raise KeyError

    Decodes exactly one complete key and rejects trailing bytes.

    KeySpec::encode

    fn KeySpec::encode(self : KeySpec, values : Array[Value]) -> Bytes raise KeyError

    Encodes a complete key in unsigned lexicographic byte order.

    KeySpec::field_range

    fn KeySpec::field_range(self : KeySpec, prefix : Array[Value], lower : Bound, upper : Bound) -> ByteRange raise KeyError

    Compiles equality on the leading fields and an interval on the next field into a single byte range. Remaining fields can have any value.

    KeySpec::from_layout

    fn KeySpec::from_layout(descriptor : Bytes) -> KeySpec raise KeyError

    Reconstructs a spec from a persisted v1 layout descriptor.

    KeySpec::layout

    fn KeySpec::layout(self : KeySpec) -> Bytes

    Store this descriptor alongside the index, outside the ordered data keys.

    KeySpec::new

    fn KeySpec::new(fields : Array[Field]) -> KeySpec raise KeyError

    KeySpec::prefix_range

    fn KeySpec::prefix_range(self : KeySpec, prefix : Array[Value]) -> ByteRange raise KeyError

    Matches complete fields at the start of a key. An empty prefix matches the entire index. Each index must use the same spec for every key.

    KeySpec::require_layout

    fn KeySpec::require_layout(self : KeySpec, descriptor : Bytes) -> Unit raise KeyError

    Checks the stored key format, field types and directions before opening an index.

    KeySpec::resume_after

    fn KeySpec::resume_after(self : KeySpec, range : ByteRange, cursor : Bytes) -> ByteRange raise KeyError

    Restricts the range to keys strictly after a complete key from that range.

    KeySpec::tuple_range

    fn KeySpec::tuple_range(self : KeySpec, prefix : Array[Value], first : TupleBound, last : TupleBound) -> ByteRange raise KeyError

    Each bounded endpoint supplies at least one field after the fixed prefix. Inclusion or exclusion applies to every key that shares that endpoint.

    ReindexPlan

    pub struct ReindexPlan {
    // private fields
    }

    A checked projection from a frozen source index to its replacement.

    ReindexPlan::new

    fn ReindexPlan::new(source : IndexContract, target : IndexContract) -> ReindexPlan raise ReindexError

    ReindexPlan::rekey

    fn ReindexPlan::rekey(self : ReindexPlan, key : Bytes) -> Bytes raise KeyError

    TupleBound

    pub(all) enum TupleBound {
    TupleUnbounded
    TupleInclusive(Array[Value])
    TupleExclusive(Array[Value])
    }

    A bound on one or more consecutive fields, in the index's configured order.

    Value

    pub(all) enum Value {
    Binary(Bytes)
    Signed(Int64)
    } derive(Eq,
    Debug
    )

    Value::equal

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

    Value::not_equal

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

    Value::to_repr