pub struct Page<T> {
pub rows: Vec<T>,
pub total: i64,
}
impl<T> Default for Page<T> {
fn default() -> Self {
Self {
rows: Vec::new(),
total: 0,
}
}
}
fn inject_total_count(sql: &str) -> String {
if let Some(idx) = sql.find(" FROM ") {
let mut out = String::with_capacity(sql.len() + 48);
out.push_str(&sql[..idx]);
out.push_str(", COUNT(*) OVER () AS \"__rustango_total\"");
out.push_str(&sql[idx..]);
out
} else {
format!(
"/* rustango: fetch_paginated_on could not splice COUNT(*) OVER () \
into the compiled SELECT — anchor ` FROM ` not found. The query \
below will run unchanged but `total` will be 0. */ {sql}"
)
}
}
pub(super) fn paginated_statement(
d: &dyn crate::sql::Dialect,
select: &crate::core::SelectQuery,
) -> Result<crate::sql::CompiledStatement, crate::sql::SqlError> {
let distinct = match select.distinct {
Some(crate::core::DistinctMode::All) => true,
Some(crate::core::DistinctMode::On(_)) => d.name() == "postgres",
None => false,
};
if select.compound.is_empty() && distinct {
let mut b = crate::sql::writers::Sql::new(d);
crate::sql::writers::write_distinct_with_total(&mut b, select)?;
return b.finish();
}
if select.compound.is_empty() {
let stmt = d.compile_select(select)?;
return Ok(crate::sql::CompiledStatement {
sql: inject_total_count(&stmt.sql),
params: stmt.params,
});
}
let mut b = crate::sql::writers::Sql::new(d);
crate::sql::writers::write_compound_with_total(&mut b, select)?;
b.finish()
}
pub(super) fn total_statement(
d: &dyn crate::sql::Dialect,
select: &crate::core::SelectQuery,
) -> Result<Option<crate::sql::CompiledStatement>, crate::sql::SqlError> {
let offset = if select.compound.is_empty() {
select.offset
} else {
select.compound_offset
};
if offset.unwrap_or(0) <= 0 {
return Ok(None);
}
let mut all = select.clone();
all.limit = None;
all.offset = None;
all.compound_limit = None;
all.compound_offset = None;
d.compile_count(&crate::core::CountQuery::from_select(all))
.map(Some)
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use crate::core::{
CompoundBranch, FieldSchema, FieldType, ModelSchema, NullsOrder, OrderItem, SelectQuery,
SetOp,
};
const ITEM: &ModelSchema = &{
const FIELDS: &[FieldSchema] = &[
FieldSchema::new("id", "id", FieldType::I64),
FieldSchema::new("name", "name", FieldType::String),
FieldSchema::new("rnk", "rnk", FieldType::I64),
];
let mut s = ModelSchema::new("PageItem", "page_item");
s.fields = FIELDS;
s
};
fn paged(op: SetOp) -> SelectQuery {
let mut q = SelectQuery::new(ITEM);
q.projection = Some(vec!["name"]);
q.compound = vec![CompoundBranch {
op,
query: Box::new(q.clone()),
}];
q.compound_order_by = vec![OrderItem::Column {
column: "rnk",
desc: false,
nulls: NullsOrder::Default,
}];
q.compound_limit = Some(2);
q
}
#[test]
fn paged_set_op_ordered_outside_the_projection() {
let d = &crate::sql::Sqlite;
let sql = super::paginated_statement(d, &paged(SetOp::UnionAll))
.expect("UNION ALL")
.sql;
assert!(
sql.starts_with(r#"SELECT "name", COUNT(*) OVER () AS "__rustango_total""#),
"{sql}"
);
assert!(
sql.contains(r#") AS "__rustango_set" ORDER BY "rnk""#),
"{sql}"
);
let err = super::paginated_statement(d, &paged(SetOp::Union)).expect_err("UNION");
assert!(
matches!(
err,
crate::sql::SqlError::SetOpOrderOutsideProjection { column: "rnk" }
),
"{err:?}"
);
}
}