use std::marker::PhantomData;
use crate::db::FlozRowBound;
use crate::db::Executor;
use crate::error::FlozError;
use crate::expr::{Expr, OrderExpr};
use crate::query::quote_table;
use crate::value::Value;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Paginated<T> {
pub items: Vec<T>,
pub total: i64,
pub page: i64,
pub per_page: i64,
pub total_pages: i64,
pub has_next: bool,
pub has_prev: bool,
}
impl<T> Paginated<T> {
pub fn new(items: Vec<T>, total: i64, page: i64, per_page: i64) -> Self {
let total_pages = if per_page > 0 && total > 0 {
(total + per_page - 1) / per_page
} else {
0
};
Self {
items,
total,
page,
per_page,
total_pages,
has_next: page < total_pages,
has_prev: page > 1,
}
}
}
pub struct PaginateQuery<T> {
table: String,
filter: Option<Expr>,
order: Vec<OrderExpr>,
page: i64,
per_page: i64,
_phantom: PhantomData<T>,
}
impl<T> PaginateQuery<T> {
pub fn new(table: &str) -> Self {
Self {
table: table.to_string(),
filter: None,
order: Vec::new(),
page: 1,
per_page: 25,
_phantom: PhantomData,
}
}
pub fn page(mut self, page: i64) -> Self {
self.page = if page < 1 { 1 } else { page };
self
}
pub fn per_page(mut self, per_page: i64) -> Self {
self.per_page = if per_page < 1 { 1 } else { per_page };
self
}
pub fn where_(mut self, expr: Expr) -> Self {
self.filter = Some(expr);
self
}
pub fn order_by(mut self, order: OrderExpr) -> Self {
self.order.push(order);
self
}
pub async fn execute(self, db: &impl Executor) -> Result<Paginated<T>, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let quoted = quote_table(&self.table);
let offset = (self.page - 1) * self.per_page;
let mut count_sql = format!("SELECT COUNT(*) as count FROM {}", quoted);
let mut count_params: Vec<Value> = Vec::new();
let mut count_idx = 0usize;
if let Some(ref filter) = self.filter {
count_sql.push_str(" WHERE ");
filter.to_sql(&mut count_sql, &mut count_params, &mut count_idx);
}
let row: CountRow = db.fetch_one(&count_sql, count_params).await?;
let total = row.count;
let mut data_sql = format!("SELECT * FROM {}", quoted);
let mut data_params: Vec<Value> = Vec::new();
let mut data_idx = 0usize;
if let Some(ref filter) = self.filter {
data_sql.push_str(" WHERE ");
filter.to_sql(&mut data_sql, &mut data_params, &mut data_idx);
}
if !self.order.is_empty() {
data_sql.push_str(" ORDER BY ");
for (i, ord) in self.order.iter().enumerate() {
if i > 0 {
data_sql.push_str(", ");
}
ord.to_sql(&mut data_sql);
}
}
data_idx += 1;
data_sql.push_str(&format!(" LIMIT ${}", data_idx));
data_params.push(Value::BigInt(self.per_page));
data_idx += 1;
data_sql.push_str(&format!(" OFFSET ${}", data_idx));
data_params.push(Value::BigInt(offset));
let items: Vec<T> = db.fetch_all(&data_sql, data_params).await?;
Ok(Paginated::new(items, total, self.page, self.per_page))
}
}
#[derive(Debug, sqlx::FromRow)]
struct CountRow {
count: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paginated_basic() {
let p: Paginated<()> = Paginated::new(vec![(); 10], 50, 1, 10);
assert_eq!(p.total, 50);
assert_eq!(p.page, 1);
assert_eq!(p.per_page, 10);
assert_eq!(p.total_pages, 5);
assert!(p.has_next);
assert!(!p.has_prev);
}
#[test]
fn paginated_last_page() {
let p: Paginated<()> = Paginated::new(vec![(); 5], 25, 3, 10);
assert_eq!(p.total_pages, 3);
assert!(!p.has_next);
assert!(p.has_prev);
}
#[test]
fn paginated_middle_page() {
let p: Paginated<()> = Paginated::new(vec![(); 10], 30, 2, 10);
assert_eq!(p.total_pages, 3);
assert!(p.has_next);
assert!(p.has_prev);
}
#[test]
fn paginated_empty() {
let p: Paginated<()> = Paginated::new(vec![], 0, 1, 10);
assert_eq!(p.total_pages, 0);
assert!(!p.has_next);
assert!(!p.has_prev);
}
#[test]
fn paginated_single_page() {
let p: Paginated<()> = Paginated::new(vec![(); 3], 3, 1, 10);
assert_eq!(p.total_pages, 1);
assert!(!p.has_next);
assert!(!p.has_prev);
}
#[test]
fn paginated_remainder_page() {
let p: Paginated<()> = Paginated::new(vec![(); 1], 21, 3, 10);
assert_eq!(p.total_pages, 3);
assert!(!p.has_next);
assert!(p.has_prev);
}
#[test]
fn paginate_query_clamps_page() {
let q = PaginateQuery::<()>::new("test").page(0);
assert_eq!(q.page, 1);
}
#[test]
fn paginate_query_clamps_per_page() {
let q = PaginateQuery::<()>::new("test").per_page(-5);
assert_eq!(q.per_page, 1);
}
#[test]
fn paginate_query_defaults() {
let q = PaginateQuery::<()>::new("users");
assert_eq!(q.page, 1);
assert_eq!(q.per_page, 25);
assert!(q.filter.is_none());
assert!(q.order.is_empty());
}
}