icydb-core 0.198.1

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: query::fluent::load::pagination
//! Responsibility: fluent paged-query wrapper APIs and cursor continuation terminals.
//! Does not own: planner semantic validation or runtime execution internals.
//! Boundary: exposes paged execution surfaces over fluent load query contracts.

use crate::{
    db::{
        PagedLoadExecution, PagedLoadExecutionWithTrace, PersistedRow,
        query::fluent::load::FluentLoadQuery,
        query::{
            intent::{IntentError, Query, QueryError},
            read_intent::{ADMIN_BATCH_ROWS, AdminBatchRequest, PageRequest},
        },
    },
    traits::{EntityKind, EntityValue},
};

///
/// PagedLoadQuery
///
/// Session-bound cursor pagination wrapper.
/// This wrapper only exposes cursor continuation and paged execution.
///

pub struct PagedLoadQuery<'a, E>
where
    E: EntityKind,
{
    inner: FluentLoadQuery<'a, E>,
}

impl<'a, E> FluentLoadQuery<'a, E>
where
    E: PersistedRow,
{
    /// Enter typed cursor-pagination mode for this request-owned page.
    ///
    /// Cursor pagination requires:
    /// - explicit `order_term(...)`
    /// - no prior raw `limit(...)`
    ///
    /// Requests are deterministic under canonical ordering, but continuation is
    /// best-effort and forward-only over live state.
    /// No snapshot/version is pinned across requests, so concurrent writes may
    /// shift page boundaries.
    pub fn page(self, request: PageRequest) -> Result<PagedLoadQuery<'a, E>, QueryError> {
        self.ensure_semantic_terminal_owns_limit(IntentError::raw_limit_before_page_terminal())?;
        self.ensure_page_request_owns_cursor()?;

        let limit = request.effective_limit();
        let cursor = request.into_cursor();
        let mut inner = self.map_query(|query| query.with_load_limit(limit));
        if let Some(cursor) = cursor {
            inner = inner.with_cursor_token(cursor);
        }

        inner.ensure_paged_mode_ready()?;

        Ok(PagedLoadQuery { inner })
    }

    /// Execute this query as cursor pagination and return items + next cursor.
    ///
    /// The returned cursor token is opaque and must be passed back through
    /// `PageRequest`.
    pub fn execute_paged(self, request: PageRequest) -> Result<PagedLoadExecution<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.page(request)?.execute()
    }

    /// Execute cursor pagination without the default bounded read-admission gate.
    ///
    /// This is for trusted maintenance/admin code that has its own caller
    /// authorization and resource policy. Application-facing reads should use
    /// `execute_paged`.
    pub fn execute_paged_trusted(
        self,
        request: PageRequest,
    ) -> Result<PagedLoadExecution<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.page(request)?.execute_trusted()
    }

    /// Execute a trusted/admin cursor batch with an engine-owned batch size.
    ///
    /// This terminal is intentionally unavailable on the normal public read
    /// lane. Callers must opt into `trusted_read_unchecked()` before invoking
    /// it, and a prior raw `limit(...)` is rejected because the batch size is
    /// owned by IcyDB.
    pub fn admin_batch(
        self,
        request: AdminBatchRequest,
    ) -> Result<PagedLoadExecution<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.ensure_semantic_terminal_owns_limit(
            IntentError::raw_limit_before_admin_batch_terminal(),
        )?;
        self.ensure_page_request_owns_cursor()?;

        if !self.trusted_read_unchecked_enabled() {
            return Err(QueryError::intent(
                IntentError::admin_batch_requires_trusted_read(),
            ));
        }

        let cursor = request.into_cursor();
        let mut inner = self.map_query(|query| query.with_load_limit(ADMIN_BATCH_ROWS));
        if let Some(cursor) = cursor {
            inner = inner.with_cursor_token(cursor);
        }

        inner.ensure_paged_mode_ready()?;

        PagedLoadQuery { inner }.execute_trusted()
    }
}

impl<E> PagedLoadQuery<'_, E>
where
    E: PersistedRow,
{
    // ------------------------------------------------------------------
    // Intent inspection
    // ------------------------------------------------------------------

    #[must_use]
    pub const fn query(&self) -> &Query<E> {
        self.inner.query()
    }

    // ------------------------------------------------------------------
    // Execution
    // ------------------------------------------------------------------

    /// Execute in cursor-pagination mode and return items + next cursor.
    ///
    /// Continuation is best-effort and forward-only over live state:
    /// deterministic per request under canonical ordering, with no
    /// snapshot/version pinned across requests.
    pub fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.execute_with_trace()
            .map(PagedLoadExecutionWithTrace::into_execution)
    }

    /// Execute in cursor-pagination mode without the default bounded read-admission gate.
    ///
    /// This is for trusted maintenance/admin code that has its own caller
    /// authorization and resource policy. Application-facing reads should use
    /// `execute`.
    pub fn execute_trusted(self) -> Result<PagedLoadExecution<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.execute_with_trace_trusted()
            .map(PagedLoadExecutionWithTrace::into_execution)
    }

    /// Execute in cursor-pagination mode and return items, next cursor,
    /// and optional execution trace details when session debug mode is enabled.
    ///
    /// Trace collection is opt-in via `DbSession::debug()` and does not
    /// change query planning or result semantics.
    pub fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        self.inner.ensure_default_read_admission()?;
        self.execute_with_trace_trusted()
    }

    /// Execute in cursor-pagination mode with trace details and without the
    /// default bounded read-admission gate.
    ///
    /// This is for trusted maintenance/admin code that has its own caller
    /// authorization and resource policy. Application-facing reads should use
    /// `execute_with_trace`.
    pub fn execute_with_trace_trusted(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
    where
        E: PersistedRow + EntityValue,
    {
        // `PagedLoadQuery` can only be constructed through `FluentLoadQuery::page`,
        // so the paged-mode validation already happened before this wrapper existed.
        self.inner.session.execute_load_query_paged_with_trace(
            self.inner.query(),
            self.inner.cursor_token.as_deref(),
        )
    }
}