moonpageflow

    Stable cursor pagination, keyset query planning, snapshot guards, and resumable traversal for MoonBit

    pagination
    cursor
    keyset
    api
    database
    resume
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    1 hour ago
    Downloads
    2

    #MoonPageFlow

    CI License: MIT

    MoonPageFlow 是面向 MoonBit 服务端、SDK 与数据工具的稳定游标分页内核。它把复合排序、唯一键、正反向游标、快照校验、数据库 seek 条件和可恢复批处理统一为一套与数据库、Web 框架无关的行为契约。

    它解决的不是“把数组切成若干页”,而是数据持续变化时 offset 分页可能重复或漏读、各项目自行编码游标容易产生不兼容行为的问题。

    #项目目标

    MoonPageFlow 负责分页语义层,而不是数据库连接层:应用声明排序和数据版本,库产生稳定页面、可传输游标与数据库 seek 计划。这样 HTTP API、GraphQL resolver、ORM 适配器和离线任务可以共享同一套边界行为。

    #核心能力

    • Int64、String、Bool、Null 复合排序键,支持升降序及显式 NULL 位置;
    • 自动追加唯一记录 ID,确保相同业务排序值仍有全序;
    • 带版本、排序元数据、快照和校验和的 URL-safe 不透明游标;
    • first/afterlast/before 双向排页及完整 PageInfo;
    • 将游标边界编译为与数据库无关的字典序 seek 分支;
    • 快照漂移、游标损坏、排序不一致、重复 ID 和资源上限的结构化错误;
    • 可持久化的批处理检查点,以及重复游标/重复记录保护;
    • offset 兼容入口和遍历审计器,用于迁移前后行为对照。
    • 稳定 O(n log n) 内存参考排序器,以及数据库适配结果预言机。

    #快速开始

    moon add TangShiJi/moonpageflow@0.1.0

    let rows = [
    @moonpageflow.page_row("event-2", [
    ("created_at", @moonpageflow.int_value(300L)),
    ]).unwrap(),
    @moonpageflow.page_row("event-1", [
    ("created_at", @moonpageflow.int_value(300L)),
    ]).unwrap(),
    ]
    let sort = [
    @moonpageflow.sort_field("created_at", direction=@moonpageflow.Descending),
    ]
    let page = @moonpageflow.paginate(
    rows,
    sort,
    @moonpageflow.first_page(limit=20).unwrap(),
    snapshot=Some("events-revision-42"),
    ).unwrap()

    for row in @moonpageflow.page_rows(page) {
    println(row.id())
    }

    完整最小示例可以直接运行:

    git clone https://github.com/TangShiJi/moonpageflow.git cd moonpageflow moon run examples/quickstart

    预期输出前两条同时间事件、has_next=true 和可继续请求的不透明游标。examples/activity_feedexamples/database_seekexamples/batch_export 分别覆盖动态列表、数据库/ORM 条件适配和断点续传导出;cmd/main 一次运行三个场景。

    #常用 API

    任务API
    声明记录和排序page_rowsort_field
    正向/反向请求first_pagelast_page
    内存参考分页paginatepage_rows
    数据库条件适配build_seek_planverify_seek_ids
    批处理恢复traversal_checkpointadvance_checkpoint
    迁移行为审计paginate_offsetaudit_traversal

    错误使用 PageErrorKind 分类,宿主可以稳定映射为 HTTP、GraphQL 或任务状态。详细语义和适配步骤见 API 使用指南

    #本地验证

    moon fmt --check moon check --target native --deny-warn moon build --target native --deny-warn moon test --target native --deny-warn moon check --target wasm-gc --deny-warn moon build --target wasm-gc --deny-warn moon test --target wasm-gc --deny-warn moon coverage analyze moon package --list

    CI 在 Windows 和 Ubuntu 上执行检查、构建、66 项测试、示例、覆盖率分析和发布包检查。验收标准见 ACCEPTANCE_CRITERIA.md,设计见 docs/design.md,兼容边界见 COMPATIBILITY.md,安全边界见 SECURITY.md

    #明确边界

    本库不连接数据库、不解析 SQL、不实现 ORM/GraphQL 服务器,也不替宿主决定事务隔离级别。游标校验和只发现误改,不承担鉴权或防伪;对外暴露游标时,应用可在其外层增加 MAC 或签名。内存参考分页器用于行为验证和小数据集;大数据集应由适配器把 SeekPlan 下推到数据库索引。

    本项目采用 OSI 认可的 MIT License,为原创实现,未复制或移植第三方源码。

    EqualityTerm

    pub struct EqualityTerm {
    field : String
    value : PageValue
    nulls : NullPlacement
    } derive(Eq,
    Debug
    )

    EqualityTerm::field

    fn EqualityTerm::field(self : EqualityTerm) -> String

    EqualityTerm::nulls

    EqualityTerm::value

    fn EqualityTerm::value(self : EqualityTerm) -> PageValue

    KeyPart

    pub struct KeyPart {
    field : String
    value : PageValue
    direction : SortDirection
    nulls : NullPlacement
    } derive(Eq,
    Debug
    )

    A typed key component captured inside a cursor.

    KeyPart::direction

    fn KeyPart::direction(self : KeyPart) -> SortDirection

    KeyPart::field

    fn KeyPart::field(self : KeyPart) -> String

    KeyPart::nulls

    fn KeyPart::nulls(self : KeyPart) -> NullPlacement

    KeyPart::value

    fn KeyPart::value(self : KeyPart) -> PageValue

    NullPlacement

    pub(all) enum NullPlacement {
    NullsFirst
    NullsLast
    } derive(Eq,
    Debug
    )

    Explicit null placement avoids database-dependent default ordering.

    OffsetPage

    pub struct OffsetPage {
    rows : Array[PageRow]
    info : OffsetPageInfo
    } derive(Eq,
    Debug
    )

    OffsetPage::info

    OffsetPage::rows

    fn OffsetPage::rows(self : OffsetPage) -> Array[PageRow]

    OffsetPageInfo

    pub struct OffsetPageInfo {
    offset : Int
    limit : Int
    total : Int
    has_previous_page : Bool
    has_next_page : Bool
    } derive(Eq,
    Debug
    )

    OffsetPageInfo::has_next_page

    fn OffsetPageInfo::has_next_page(self : OffsetPageInfo) -> Bool

    OffsetPageInfo::has_previous_page

    fn OffsetPageInfo::has_previous_page(self : OffsetPageInfo) -> Bool

    OffsetPageInfo::limit

    fn OffsetPageInfo::limit(self : OffsetPageInfo) -> Int

    OffsetPageInfo::offset

    fn OffsetPageInfo::offset(self : OffsetPageInfo) -> Int

    OffsetPageInfo::total

    fn OffsetPageInfo::total(self : OffsetPageInfo) -> Int

    OrderViolation

    pub struct OrderViolation {
    previous_id : String
    current_id : String
    } derive(Eq,
    Debug
    )

    OrderViolation::current_id

    fn OrderViolation::current_id(self : OrderViolation) -> String

    OrderViolation::previous_id

    fn OrderViolation::previous_id(self : OrderViolation) -> String

    PageCursor

    pub struct PageCursor {
    version : Int
    position : PagePosition
    snapshot : String?
    } derive(Eq,
    Debug
    )

    Decoded cursor data. Snapshot is supplied by the host, for example a database revision, event offset, or content hash.

    PageCursor::position

    fn PageCursor::position(self : PageCursor) -> PagePosition

    PageCursor::snapshot

    fn PageCursor::snapshot(self : PageCursor) -> String?

    PageCursor::version

    fn PageCursor::version(self : PageCursor) -> Int

    PageEdge

    pub struct PageEdge {
    row : PageRow
    cursor : String
    } derive(Eq,
    Debug
    )

    One result edge with a cursor that resumes after or before this row.

    PageEdge::cursor

    fn PageEdge::cursor(self : PageEdge) -> String

    PageEdge::row

    fn PageEdge::row(self : PageEdge) -> PageRow

    PageError

    pub struct PageError {
    kind : PageErrorKind
    field : String
    message : String
    } derive(Eq,
    Debug
    )

    Structured failure suitable for API error mapping and test assertions.

    PageError::field

    fn PageError::field(self : PageError) -> String

    PageError::kind

    fn PageError::kind(self : PageError) -> PageErrorKind

    PageError::message

    fn PageError::message(self : PageError) -> String

    PageErrorKind

    pub(all) enum PageErrorKind {
    EmptyRowId
    DuplicateRowId
    EmptySortField
    DuplicateSortField
    MissingSortValue
    InvalidPageSize
    TooManySortFields
    TooManyRows
    InvalidCursor
    UnsupportedCursorVersion
    CursorChecksumMismatch
    CursorSortMismatch
    CursorTooLong
    SnapshotMismatch
    RepeatedCursor
    DuplicateTraversalItem
    InvalidOffset
    } derive(Eq,
    Debug
    )

    PageInfo

    pub struct PageInfo {
    has_previous_page : Bool
    has_next_page : Bool
    start_cursor : String?
    end_cursor : String?
    snapshot : String?
    } derive(Eq,
    Debug
    )

    Navigation metadata follows the common connection model while remaining independent of GraphQL or any web framework.

    PageInfo::end_cursor

    fn PageInfo::end_cursor(self : PageInfo) -> String?

    PageInfo::has_next_page

    fn PageInfo::has_next_page(self : PageInfo) -> Bool

    PageInfo::has_previous_page

    fn PageInfo::has_previous_page(self : PageInfo) -> Bool

    PageInfo::snapshot

    fn PageInfo::snapshot(self : PageInfo) -> String?

    PageInfo::start_cursor

    fn PageInfo::start_cursor(self : PageInfo) -> String?

    PageLimits

    pub struct PageLimits {
    default_size : Int
    max_size : Int
    max_sort_fields : Int
    max_cursor_chars : Int
    max_rows : Int
    } derive(Eq,
    Debug
    )

    Limits applied before decoding cursors or sorting untrusted input.

    PageLimits::default_size

    fn PageLimits::default_size(self : PageLimits) -> Int

    PageLimits::max_cursor_chars

    fn PageLimits::max_cursor_chars(self : PageLimits) -> Int

    PageLimits::max_rows

    fn PageLimits::max_rows(self : PageLimits) -> Int

    PageLimits::max_size

    fn PageLimits::max_size(self : PageLimits) -> Int

    PageLimits::max_sort_fields

    fn PageLimits::max_sort_fields(self : PageLimits) -> Int

    PageMode

    pub(all) enum PageMode {
    ForwardPage
    BackwardPage
    } derive(Eq,
    Debug
    )

    Direction in which a page is requested from a cursor boundary.

    PagePosition

    pub struct PagePosition {
    parts : Array[KeyPart]
    tie_breaker : String
    } derive(Eq,
    Debug
    )

    Stable position consists of declared sort values and a unique record id.

    PagePosition::parts

    fn PagePosition::parts(self : PagePosition) -> Array[KeyPart]

    PagePosition::tie_breaker

    fn PagePosition::tie_breaker(self : PagePosition) -> String

    PageRequest

    pub struct PageRequest {
    mode : PageMode
    limit : Int
    cursor : String?
    } derive(Eq,
    Debug
    )

    Validated page request. cursor is interpreted as an exclusive boundary.

    PageRequest::cursor

    fn PageRequest::cursor(self : PageRequest) -> String?

    PageRequest::limit

    fn PageRequest::limit(self : PageRequest) -> Int

    PageRequest::mode

    fn PageRequest::mode(self : PageRequest) -> PageMode

    PageResult

    pub struct PageResult {
    edges : Array[PageEdge]
    info : PageInfo
    } derive(Eq,
    Debug
    )

    Complete page returned by the in-memory reference engine.

    PageResult::edges

    fn PageResult::edges(self : PageResult) -> Array[PageEdge]

    PageResult::info

    fn PageResult::info(self : PageResult) -> PageInfo

    PageRow

    pub struct PageRow {
    id : String
    values : Array[(String, PageValue)]
    } derive(Eq,
    Debug
    )

    Format-neutral record used by the reference paginator. Database adapters may use the same ordering and cursor APIs without materializing all rows.

    PageRow::id

    fn PageRow::id(self : PageRow) -> String

    PageRow::value

    fn PageRow::value(self : PageRow, name : String) -> PageValue?

    PageRow::values

    fn PageRow::values(self : PageRow) -> Array[(String, PageValue)]

    PageValue

    pub(all) enum PageValue {
    NullValue
    IntValue(Int64)
    TextValue(String)
    BoolValue(Bool)
    } derive(Eq,
    Debug
    )

    Values that may participate in a portable ordering key.

    PaginationAudit

    pub struct PaginationAudit {
    observed_count : Int
    unique_count : Int
    duplicate_ids : Array[String]
    missing_ids : Array[String]
    unexpected_ids : Array[String]
    order_violations : Array[OrderViolation]
    } derive(Eq,
    Debug
    )

    Evidence produced from a completed or partial page traversal.

    PaginationAudit::duplicate_ids

    fn PaginationAudit::duplicate_ids(self : PaginationAudit) -> Array[String]

    PaginationAudit::is_clean

    fn PaginationAudit::is_clean(self : PaginationAudit) -> Bool

    PaginationAudit::missing_ids

    fn PaginationAudit::missing_ids(self : PaginationAudit) -> Array[String]

    PaginationAudit::observed_count

    fn PaginationAudit::observed_count(self : PaginationAudit) -> Int

    PaginationAudit::order_violations

    fn PaginationAudit::order_violations(self : PaginationAudit) -> Array[OrderViolation]

    PaginationAudit::unexpected_ids

    fn PaginationAudit::unexpected_ids(self : PaginationAudit) -> Array[String]

    PaginationAudit::unique_count

    fn PaginationAudit::unique_count(self : PaginationAudit) -> Int

    SeekBranch

    pub struct SeekBranch {
    equal_prefix : Array[EqualityTerm]
    field : String
    value : PageValue
    comparison : SeekComparison
    nulls : NullPlacement
    } derive(Eq,
    Debug
    )

    One branch of a lexicographic keyset predicate. Branches are OR-ed; terms in equal_prefix are AND-ed before the branch comparison.

    SeekBranch::comparison

    fn SeekBranch::comparison(self : SeekBranch) -> SeekComparison

    SeekBranch::equal_prefix

    fn SeekBranch::equal_prefix(self : SeekBranch) -> Array[EqualityTerm]

    SeekBranch::field

    fn SeekBranch::field(self : SeekBranch) -> String

    SeekBranch::nulls

    fn SeekBranch::nulls(self : SeekBranch) -> NullPlacement

    SeekBranch::value

    fn SeekBranch::value(self : SeekBranch) -> PageValue

    SeekComparison

    pub(all) enum SeekComparison {
    GreaterThan
    LessThan
    } derive(Eq,
    Debug
    )

    Abstract comparison emitted for a database or search-index adapter.

    SeekConformance

    pub struct SeekConformance {
    expected_ids : Array[String]
    actual_ids : Array[String]
    } derive(Eq,
    Debug
    )

    Result of comparing a database or ORM adapter with the reference seek semantics. Keeping the expected and actual ids makes CI failures actionable.

    SeekConformance::actual_ids

    fn SeekConformance::actual_ids(self : SeekConformance) -> Array[String]

    SeekConformance::expected_ids

    fn SeekConformance::expected_ids(self : SeekConformance) -> Array[String]

    SeekConformance::matches

    fn SeekConformance::matches(self : SeekConformance) -> Bool

    SeekPlan

    pub struct SeekPlan {
    branches : Array[SeekBranch]
    reverse_query_order : Bool
    } derive(Eq,
    Debug
    )

    SeekPlan::branches

    fn SeekPlan::branches(self : SeekPlan) -> Array[SeekBranch]

    SeekPlan::reverse_query_order

    fn SeekPlan::reverse_query_order(self : SeekPlan) -> Bool

    SortDirection

    pub(all) enum SortDirection {
    Ascending
    Descending
    } derive(Eq,
    Debug
    )

    Sort direction for one key component.

    SortField

    pub struct SortField {
    name : String
    direction : SortDirection
    nulls : NullPlacement
    } derive(Eq,
    Debug
    )

    One named sort key. Record id is always appended as the final unique tie-breaker and therefore must not appear here.

    SortField::direction

    fn SortField::direction(self : SortField) -> SortDirection

    SortField::name

    fn SortField::name(self : SortField) -> String

    SortField::nulls

    fn SortField::nulls(self : SortField) -> NullPlacement

    TraversalCheckpoint

    pub struct TraversalCheckpoint {
    cursor : String?
    snapshot : String?
    pages : Int
    items : Int
    seen_ids : Array[String]
    } derive(Eq,
    Debug
    )

    Serializable logical state for a long-running page scan. Hosts persist the scalar fields in their own format; no file or network IO is performed here.

    TraversalCheckpoint::cursor

    fn TraversalCheckpoint::cursor(self : TraversalCheckpoint) -> String?

    TraversalCheckpoint::items

    fn TraversalCheckpoint::items(self : TraversalCheckpoint) -> Int

    TraversalCheckpoint::next_request

    fn TraversalCheckpoint::next_request(self : TraversalCheckpoint, limit : Int, limits? : PageLimits) -> Result[PageRequest, PageError]

    TraversalCheckpoint::pages

    fn TraversalCheckpoint::pages(self : TraversalCheckpoint) -> Int

    TraversalCheckpoint::seen_ids

    fn TraversalCheckpoint::seen_ids(self : TraversalCheckpoint) -> Array[String]

    TraversalCheckpoint::snapshot

    fn TraversalCheckpoint::snapshot(self : TraversalCheckpoint) -> String?

    advance_checkpoint

    fn advance_checkpoint(checkpoint : TraversalCheckpoint, page : PageResult) -> Result[TraversalCheckpoint, PageError]

    Advance only when the page matches the checkpoint snapshot, has a fresh end cursor, and contains no record already observed by this traversal.

    audit_traversal

    fn audit_traversal(observed : Array[PageRow], expected : Array[PageRow], sort : Array[SortField], limits? : PageLimits) -> Result[PaginationAudit, PageError]

    Audit a sequence collected from multiple pages against the records expected at traversal start. This makes mutation-induced offset anomalies observable in tests and production export verification.

    bool_value

    fn bool_value(value : Bool) -> PageValue

    build_seek_plan

    fn build_seek_plan(position : PagePosition, sort : Array[SortField], mode : PageMode) -> Result[SeekPlan, PageError]

    Build (k1 op v1) OR (k1=v1 AND k2 op v2) ... without generating SQL. The host maps fields and operators to its typed query API, avoiding string interpolation and keeping this package storage-engine neutral.

    compare_page_values

    fn compare_page_values(left : PageValue, right : PageValue, direction : SortDirection, nulls : NullPlacement) -> Int

    compare_row_to_position

    fn compare_row_to_position(row : PageRow, position : PagePosition, sort : Array[SortField]) -> Result[Int, PageError]

    compare_rows

    fn compare_rows(left : PageRow, right : PageRow, sort : Array[SortField]) -> Result[Int, PageError]

    decode_cursor

    fn decode_cursor(token : String, limits? : PageLimits) -> Result[PageCursor, PageError]

    Decode and validate cursor syntax, version, checksum, typed values, and resource limits. Sort compatibility is checked when the cursor is applied.

    encode_cursor

    fn encode_cursor(position : PagePosition, snapshot? : String?) -> String

    Encode a position as an ASCII and URL-safe opaque token. The checksum is for corruption detection, not cryptographic authentication.

    expected_seek_ids

    fn expected_seek_ids(input : Array[PageRow], boundary : PagePosition, sort : Array[SortField], mode : PageMode, limit : Int, limits? : PageLimits) -> Result[Array[String], PageError]

    Return the canonical ids that a database seek query must return. This is a deliberately small reference oracle for adapter tests; production adapters still execute their query inside the database.

    first_page

    fn first_page(limit? : Int, after? : String?, limits? : PageLimits) -> Result[PageRequest, PageError]

    format_audit

    fn format_audit(audit : PaginationAudit) -> String

    int_value

    fn int_value(value : Int64) -> PageValue

    last_page

    fn last_page(limit? : Int, before? : String?, limits? : PageLimits) -> Result[PageRequest, PageError]

    null_value

    fn null_value() -> PageValue

    page_limits

    fn page_limits(default_size? : Int, max_size? : Int, max_sort_fields? : Int, max_cursor_chars? : Int, max_rows? : Int) -> PageLimits

    page_row

    fn page_row(id : String, values : Array[(String, PageValue)]) -> Result[PageRow, PageError]

    page_rows

    fn page_rows(page : PageResult) -> Array[PageRow]

    paginate

    fn paginate(input : Array[PageRow], sort : Array[SortField], request : PageRequest, snapshot? : String?, limits? : PageLimits) -> Result[PageResult, PageError]

    Reference keyset paginator. Production database adapters can execute a seek plan instead of materializing rows while retaining identical cursor rules.

    paginate_offset

    fn paginate_offset(input : Array[PageRow], sort : Array[SortField], offset : Int, limit : Int, limits? : PageLimits) -> Result[OffsetPage, PageError]

    Compatibility mode for APIs that expose numeric pages. It shares sort and limit validation but cannot guarantee mutation stability like keyset mode.

    position_for

    fn position_for(row : PageRow, sort : Array[SortField]) -> Result[PagePosition, PageError]

    restore_checkpoint

    fn restore_checkpoint(cursor : String?, snapshot : String?, pages : Int, items : Int, seen_ids : Array[String]) -> Result[TraversalCheckpoint, PageError]

    sort_field

    fn sort_field(name : String, direction? : SortDirection, nulls? : NullPlacement) -> SortField

    sort_rows

    fn sort_rows(input : Array[PageRow], sort : Array[SortField], limits? : PageLimits) -> Result[Array[PageRow], PageError]

    Stable bottom-up merge sort keeps the reference engine deterministic while avoiding quadratic behavior on larger in-memory result sets. Database integrations use the same comparison contract through seek plans.

    text_value

    fn text_value(value : String) -> PageValue

    traversal_checkpoint

    fn traversal_checkpoint(snapshot? : String?) -> TraversalCheckpoint

    traversal_complete

    fn traversal_complete(page : PageResult) -> Bool

    validate_sort

    fn validate_sort(sort : Array[SortField], limits? : PageLimits) -> Result[Unit, PageError]

    verify_seek_ids

    fn verify_seek_ids(actual_ids : Array[String], input : Array[PageRow], boundary : PagePosition, sort : Array[SortField], mode : PageMode, limit : Int, limits? : PageLimits) -> Result[SeekConformance, PageError]

    Compare ids returned by a real adapter with the canonical reference page.