use std::num::NonZeroUsize;
use toasty::stmt::{List, Query, Value};
use topcoat::{Result, context::Cx};
use super::{Cursor, Table, TableState};
pub const DEFAULT_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(25).unwrap();
pub const DEFAULT_PER_PAGE_OPTIONS: [usize; 4] = [10, 25, 50, 100];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Pagination {
#[default]
Pages,
Simple,
Cursor,
}
#[derive(Debug, Clone)]
pub struct TablePage<M> {
pub rows: Vec<M>,
pub next_cursor: Option<String>,
pub prev_cursor: Option<String>,
pub position: Option<PagePosition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PagePosition {
pub page: u32,
pub per_page: usize,
pub total: Option<u64>,
pub has_next: bool,
}
impl PagePosition {
pub fn last_page(&self) -> Option<u32> {
let per_page = u64::try_from(self.per_page).ok()?.max(1);
let pages = self.total?.div_ceil(per_page).max(1);
Some(u32::try_from(pages).unwrap_or(u32::MAX))
}
}
impl<M> From<Vec<M>> for TablePage<M> {
fn from(rows: Vec<M>) -> Self {
Self {
rows,
next_cursor: None,
prev_cursor: None,
position: None,
}
}
}
impl<M: toasty::schema::Model> TablePage<M> {
pub fn from_toasty_page(page: toasty::stmt::Page<M>) -> Result<Self> {
Ok(Self {
rows: page.items,
next_cursor: page
.next_cursor
.as_ref()
.map(crate::toasty_compat::cursor::encode)
.transpose()?,
prev_cursor: page
.prev_cursor
.as_ref()
.map(crate::toasty_compat::cursor::encode)
.transpose()?,
position: None,
})
}
}
impl<M> TablePage<M>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
pub async fn load(
cx: &Cx,
table: &Table<M>,
query: Query<List<M>>,
state: &TableState,
) -> Result<Self> {
let refused = table.refused_filters(cx, state).await?;
let page = match table.pagination_mode() {
Pagination::Pages => Self::load_numbered(cx, table, query, state, &refused, true).await,
Pagination::Simple => {
Self::load_numbered(cx, table, query, state, &refused, false).await
}
Pagination::Cursor => Self::load_cursor(cx, table, query, state, &refused).await,
}?;
table
.restrict_relations(cx, &page.rows, &mut crate::db::db(cx)?)
.await?;
Ok(page)
}
async fn load_numbered(
cx: &Cx,
table: &Table<M>,
query: Query<List<M>>,
state: &TableState,
refused: &[String],
counted: bool,
) -> Result<Self> {
let mut db = crate::db::db(cx)?;
let narrowed = table.narrow(cx, &db, query, state, refused);
let per_page = table.page_size_for(state);
let total = if counted {
Some(
narrowed
.clone()
.count()
.exec(&mut db)
.await
.map_err(crate::error::unavailable)?,
)
} else {
None
};
let asked = state.page.unwrap_or(1).max(1);
let page = match total {
Some(total) => {
let position = PagePosition {
page: asked,
per_page,
total: Some(total),
has_next: false,
};
asked.min(position.last_page().unwrap_or(1))
}
None => asked,
};
let offset = usize::try_from(page - 1)
.unwrap_or(usize::MAX)
.saturating_mul(per_page);
let fetch = if counted { per_page } else { per_page + 1 };
let mut ordered = narrowed;
ordered.set_order_by(toasty::stmt::OrderBy {
exprs: table.offset_order_bys_for(state),
});
let mut rows = table
.include_relations(ordered)
.limit(fetch)
.offset(offset)
.exec(&mut db)
.await
.map_err(crate::error::unavailable)?;
let has_next = match total {
Some(total) => u64::try_from(offset + rows.len()).is_ok_and(|seen| seen < total),
None => rows.len() > per_page,
};
rows.truncate(per_page);
Ok(Self {
rows,
next_cursor: None,
prev_cursor: None,
position: Some(PagePosition {
page,
per_page,
total,
has_next,
}),
})
}
async fn load_cursor(
cx: &Cx,
table: &Table<M>,
query: Query<List<M>>,
state: &TableState,
refused: &[String],
) -> Result<Self> {
let mut db = crate::db::db(cx)?;
let base_query = table.apply_declaration(cx, &db, query, state, refused);
let query = table.include_relations(base_query.clone());
let per_page = table.page_size_for(state);
let mut paginated = toasty::stmt::Paginate::new(query, per_page);
match &state.cursor {
Some(Cursor::After(token)) => {
paginated = paginated.after(crate::toasty_compat::cursor::decode(token)?);
}
Some(Cursor::Before(token)) => {
paginated = paginated.before(crate::toasty_compat::cursor::decode(token)?);
}
None => {}
}
let loaded = paginated
.exec(&mut db)
.await
.map_err(|error| reject_cursor(error.into(), state))?;
let mut page = TablePage::from_toasty_page(loaded)?;
if matches!(state.cursor, Some(Cursor::Before(_))) {
if let Some(cursor) = page.prev_cursor.clone() {
let past = Past::Before(crate::toasty_compat::cursor::decode(&cursor)?);
if !row_exists_past(&mut db, base_query, past)
.await
.map_err(crate::error::unavailable)?
{
page.prev_cursor = None;
}
}
} else if page.rows.len() == per_page {
if let Some(cursor) = page.next_cursor.clone() {
let past = Past::After(crate::toasty_compat::cursor::decode(&cursor)?);
if !row_exists_past(&mut db, base_query, past)
.await
.map_err(crate::error::unavailable)?
{
page.next_cursor = None;
}
}
} else {
page.next_cursor = None;
}
Ok(page)
}
}
pub(crate) enum Past {
After(Value),
Before(Value),
}
pub(crate) async fn row_exists_past<M>(
db: &mut toasty::Db,
query: Query<List<M>>,
past: Past,
) -> toasty::Result<bool>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
let probe = toasty::stmt::Paginate::new(query, 1);
let probe = match past {
Past::After(cursor) => probe.after(cursor),
Past::Before(cursor) => probe.before(cursor),
};
Ok(!probe.exec(db).await?.items.is_empty())
}
fn reject_cursor(error: topcoat::Error, state: &TableState) -> topcoat::Error {
let cursored = state.cursor.is_some();
let rejected = error
.downcast_ref::<toasty::Error>()
.is_some_and(toasty::Error::is_invalid_statement);
if cursored && rejected {
crate::toasty_compat::cursor::rejected(&error)
} else {
crate::error::unavailable(error)
}
}