From a6218d13c3874aefa4132967babb7d36a7fc8d12 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Fri, 10 Jan 2025 23:20:11 +0000 Subject: [PATCH 1/3] feat(core): Massive update to Pagination - Breaking changes --- Cargo.toml | 5 + examples/pagination.rs | 57 +++++++++++ geekorm-core/src/backends/mod.rs | 38 +++++++ geekorm-core/src/error.rs | 5 + geekorm-core/src/lib.rs | 4 +- geekorm-core/src/queries/builder.rs | 4 +- geekorm-core/src/queries/mod.rs | 2 + geekorm-core/src/queries/pages.rs | 100 ++++++++++++++---- geekorm-core/src/queries/pagination.rs | 136 +++++++++++++++++++++++++ src/lib.rs | 4 +- 10 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 examples/pagination.rs create mode 100644 geekorm-core/src/queries/pagination.rs diff --git a/Cargo.toml b/Cargo.toml index 2d51270..27d88ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,11 @@ name = "chrono" path = "./examples/chrono.rs" required-features = ["all", "libsql"] +[[example]] +name = "pagination" +path = "./examples/pagination.rs" +required-features = ["all", "libsql", "pagination"] + [[example]] name = "turso-libsql" path = "./examples/turso-libsql/src/main.rs" diff --git a/examples/pagination.rs b/examples/pagination.rs new file mode 100644 index 0000000..e9ffc08 --- /dev/null +++ b/examples/pagination.rs @@ -0,0 +1,57 @@ +//! # Pagination Example +//! +//! This example demonstrates how to use the `chrono` crate with `geekorm`. +use anyhow::Result; +use geekorm::{prelude::*, GEEKORM_BANNER, GEEKORM_VERSION}; + +#[derive(Table, Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct Projects { + #[geekorm(primary_key, auto_increment)] + pub id: PrimaryKey, + #[geekorm(unique)] + pub name: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + #[cfg(debug_assertions)] + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + + println!("{} v{}\n", GEEKORM_BANNER, GEEKORM_VERSION); + let connection = create_projects().await?; + + let mut page = Projects::paginate(&connection).await?; + + /// Get the first page of projects + let mut projects = page.next(&connection).await?; + assert_eq!(page.page(), 0); + for project in &projects { + println!("Project: {}", project.name); + } + + projects = page.next(&connection).await?; + assert_eq!(projects.len(), 100); + assert_eq!(page.page(), 1); + + Ok(()) +} + +// Helper function to create 1000 projects +async fn create_projects() -> Result { + // Initialize an in-memory database + let db = libsql::Builder::new_local(":memory:").build().await?; + let connection = db.connect()?; + Projects::create_table(&connection).await?; + + for pname in 1..=1000 { + let mut prj = Projects::new(format!("geekorm-{}", pname)); + prj.save(&connection).await?; + } + + let total = Projects::total(&connection).await?; + assert_eq!(total, 1000); + + Ok(connection) +} diff --git a/geekorm-core/src/backends/mod.rs b/geekorm-core/src/backends/mod.rs index 94e368e..45795b2 100644 --- a/geekorm-core/src/backends/mod.rs +++ b/geekorm-core/src/backends/mod.rs @@ -118,6 +118,40 @@ where .await } + /// Fetch all rows from the table + #[allow(async_fn_in_trait, unused_variables)] + async fn all(connection: &'a C) -> Result, crate::Error> { + C::query::( + connection, + Self::query_select().table(Self::table()).build()?, + ) + .await + } + + /// Fetch by Page + #[cfg(feature = "pagination")] + #[allow(async_fn_in_trait, unused_variables)] + async fn page(connection: &'a C, page: &crate::Page) -> Result, crate::Error> { + C::query::( + connection, + QueryBuilder::select() + .table(Self::table()) + .page(page) + .build()?, + ) + .await + } + + /// Create a new Pagination instance with the current table and fetch + /// total number of rows + #[cfg(feature = "pagination")] + #[allow(async_fn_in_trait, unused_variables)] + async fn paginate(connection: &'a C) -> Result, crate::Error> { + let mut page = crate::Pagination::new(); + page.set_total(Self::total(connection).await? as u32); + Ok(page) + } + /// Update the current object in the database #[allow(async_fn_in_trait, unused_variables)] async fn update(&mut self, connection: &'a C) -> Result<(), crate::Error> { @@ -139,6 +173,10 @@ where async fn fetch(&mut self, connection: &'a C) -> Result<(), crate::Error>; /// Fetch all rows from the database + #[deprecated( + since = "0.8.4", + note = "Please use the `all` method instead of `fetch_all`" + )] #[allow(async_fn_in_trait, unused_variables)] async fn fetch_all(connection: &'a C) -> Result, crate::Error> { C::query::( diff --git a/geekorm-core/src/error.rs b/geekorm-core/src/error.rs index 69b5c9e..b5d0108 100644 --- a/geekorm-core/src/error.rs +++ b/geekorm-core/src/error.rs @@ -19,6 +19,11 @@ pub enum Error { #[error("No Rows Found in the database for the query")] NoRowsFound, + /// Pagination Error + #[cfg(feature = "pagination")] + #[error("Pagination Error: {0}")] + PaginationError(String), + /// Not Implemented #[error("Not Implemented")] NotImplemented, diff --git a/geekorm-core/src/lib.rs b/geekorm-core/src/lib.rs index f66a51f..aacf951 100644 --- a/geekorm-core/src/lib.rs +++ b/geekorm-core/src/lib.rs @@ -24,7 +24,9 @@ pub use crate::builder::keys::{ForeignKey, PrimaryKey}; pub use crate::builder::table::Table; pub use crate::builder::values::{Value, Values}; #[cfg(feature = "pagination")] -pub use crate::queries::pages::Pagination; +pub use crate::queries::pages::Page; +#[cfg(feature = "pagination")] +pub use crate::queries::pagination::Pagination; pub use crate::queries::{Query, QueryBuilder}; #[cfg(feature = "two-factor-auth")] pub use crate::utils::tfa::TwoFactorAuth; diff --git a/geekorm-core/src/queries/builder.rs b/geekorm-core/src/queries/builder.rs index 9075e76..b891a18 100644 --- a/geekorm-core/src/queries/builder.rs +++ b/geekorm-core/src/queries/builder.rs @@ -1,5 +1,5 @@ #[cfg(feature = "pagination")] -use super::pages::Pagination; +use super::pages::Page; use crate::builder::{ joins::{TableJoin, TableJoinOptions, TableJoins}, models::{QueryCondition, QueryOrder, QueryType, WhereCondition}, @@ -324,7 +324,7 @@ impl QueryBuilder { /// Add a page to the query #[cfg(feature = "pagination")] - pub fn page(mut self, page: &Pagination) -> Self { + pub fn page(mut self, page: &Page) -> Self { self.offset = Some(page.offset() as usize); self.limit = Some(page.limit as usize); self diff --git a/geekorm-core/src/queries/mod.rs b/geekorm-core/src/queries/mod.rs index 0dcd93d..5fbcde0 100644 --- a/geekorm-core/src/queries/mod.rs +++ b/geekorm-core/src/queries/mod.rs @@ -4,6 +4,8 @@ pub mod builder; #[cfg(feature = "pagination")] pub mod pages; +#[cfg(feature = "pagination")] +pub mod pagination; /// The Query Module pub mod query; diff --git a/geekorm-core/src/queries/pages.rs b/geekorm-core/src/queries/pages.rs index 5b6c7e0..de51ddb 100644 --- a/geekorm-core/src/queries/pages.rs +++ b/geekorm-core/src/queries/pages.rs @@ -3,7 +3,9 @@ /// Default limit for max page size const DEFAULT_LIMIT: u32 = 100; -/// Pagination struct +/// Page struct for pagination. +/// +/// This is a simple struct to handle pagination for queries. /// /// ```rust /// # use geekorm::prelude::*; @@ -17,9 +19,8 @@ const DEFAULT_LIMIT: u32 = 100; /// } /// /// # fn main() { -/// // Create a new Pagination instance -/// let mut page = Pagination::new(); -/// # assert_eq!(page.page(), 0); +/// // Create a new Page instance +/// let mut page = Page::new(); /// # assert_eq!(page.limit(), 100); /// # assert_eq!(page.offset(), 0); /// @@ -45,10 +46,10 @@ const DEFAULT_LIMIT: u32 = 100; /// # "SELECT id, username, age, postcode FROM Users WHERE username = ? ORDER BY age ASC LIMIT 100 OFFSET 100;" /// # ); /// -/// let page_max = Pagination::from((1, 10_000)); +/// let page_max = Page::from((1, 10_000)); /// # assert_eq!(page_max.limit(), 100); /// -/// let option_page = Pagination::from((Some(5), Some(10))); +/// let option_page = Page::from((Some(5), Some(10))); /// # assert_eq!(option_page.page(), 5); /// # assert_eq!(option_page.limit(), 10); /// # assert_eq!(option_page.offset(), 50); @@ -56,22 +57,29 @@ const DEFAULT_LIMIT: u32 = 100; /// # } /// ``` #[derive(Debug)] -pub struct Pagination { +pub struct Page { pub(crate) page: u32, pub(crate) limit: u32, + pub(crate) total: u32, } -impl Pagination { - /// Create a new Pagination instance +impl Page { + /// Create a new Page instance pub fn new() -> Self { - Pagination { + Page { page: 0, limit: DEFAULT_LIMIT, + total: 0, } } /// Update current page to the next page pub fn next(&mut self) { - self.page += 1; + // Don't overflow the page number, reset to 0 + if self.page == u32::MAX { + self.page = 0; + } else { + self.page += 1; + } } /// Update current page to the previous page pub fn prev(&mut self) { @@ -89,33 +97,81 @@ impl Pagination { } /// Offset for the query pub fn offset(&self) -> u32 { - self.page * self.limit + if self.page == u32::MAX { + 0 + } else { + self.page * self.limit + } + } + /// Total number of pages + pub fn pages(&self) -> u32 { + if self.total == 0 { + 0 + } else { + (self.total as f64 / self.limit as f64).ceil() as u32 + } + } + /// Set the total number of rows + pub fn set_total(&mut self, total: u32) { + self.total = total; + } + + /// Get the maximum number of pages based on the total number of rows + pub fn max(&self) -> u32 { + if self.total == 0 { + 0 + } else { + (self.total as f64 / self.limit as f64).ceil() as u32 + } } } -impl Default for Pagination { +impl Default for Page { fn default() -> Self { - Pagination { - page: 0, + Page { + page: u32::MAX, limit: DEFAULT_LIMIT, + total: 0, } } } -impl From<(u32, u32)> for Pagination { +impl From<(u32, u32)> for Page { fn from(p: (u32, u32)) -> Self { let limit = if p.1 > DEFAULT_LIMIT { DEFAULT_LIMIT } else { p.1 }; - Pagination { page: p.0, limit } + Page { + page: p.0, + limit, + ..Default::default() + } } } -impl From<(Option, Option)> for Pagination { +impl From<(Option, Option)> for Page { fn from(value: (Option, Option)) -> Self { - let mut page = Pagination::new(); + let mut page = Page::new(); + if let Some(p) = value.0 { + page.page = p; + } + if let Some(l) = value.1 { + if l > DEFAULT_LIMIT { + page.limit = DEFAULT_LIMIT; + } else { + page.limit = l; + } + } + page + } +} + +/// Implement From for Page (page, limit, total) +impl From<(Option, Option, u32)> for Page { + fn from(value: (Option, Option, u32)) -> Self { + let mut page = Page::new(); if let Some(p) = value.0 { page.page = p; } @@ -126,15 +182,17 @@ impl From<(Option, Option)> for Pagination { page.limit = l; } } + page.total = value.2; page } } -impl From for Pagination { +impl From for Page { fn from(value: u32) -> Self { - Pagination { + Page { page: value, limit: DEFAULT_LIMIT, + ..Default::default() } } } diff --git a/geekorm-core/src/queries/pagination.rs b/geekorm-core/src/queries/pagination.rs new file mode 100644 index 0000000..e143a33 --- /dev/null +++ b/geekorm-core/src/queries/pagination.rs @@ -0,0 +1,136 @@ +//! # Pagination + +use super::pages::Page; +use crate::{GeekConnection, QueryBuilderTrait, TableBuilder}; + +/// A struct for paginating results +/// +/// # Example +/// +/// ```no_run +/// # #[cfg(feature = "libsql")] { +/// # use geekorm::prelude::*; +/// +/// #[derive(Table, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +/// pub struct Users { +/// pub id: PrimaryKeyInteger, +/// pub username: String, +/// pub age: i32, +/// pub postcode: Option, +/// } +/// +/// pub type UserPage = Pagination; +/// +/// # fn main() { +/// // Create a new Page instance +/// let mut page = UserPage::new(); +/// +/// // Update the page to the next page +/// let results = page.next(); +/// # assert_eq!(page.limit(), 100); +/// # assert_eq!(page.page(), 0); +/// +/// # } +/// # } +/// ``` +pub struct Pagination +where + T: TableBuilder + + QueryBuilderTrait + + serde::Serialize + + for<'de> serde::Deserialize<'de> + + Sized, +{ + phantom: std::marker::PhantomData, + page: Page, +} + +impl Pagination +where + T: TableBuilder + + QueryBuilderTrait + + serde::Serialize + + for<'de> serde::Deserialize<'de> + + Sized, +{ + /// Create a new Pagination instance + pub fn new() -> Self { + Self::default() + } + /// Get the current page + pub fn page(&self) -> u32 { + self.page.page() + } + /// Get the limit + pub fn limit(&self) -> u32 { + self.page.limit() + } + /// Get the total number of items + pub fn total(&self) -> u32 { + self.page.total + } + /// Set the total number of items + pub fn set_total(&mut self, total: u32) { + self.page.set_total(total); + } + + /// Get the current page results + pub async fn get<'a, C>(&mut self, connection: &'a C) -> Result, crate::Error> + where + C: GeekConnection + 'a, + { + // Gets the total number of rows if it hasn't been set + if self.page.total == 0 { + self.page + .set_total(C::row_count(connection, T::query_count().build()?).await? as u32); + } + Ok(C::query(connection, T::query_select().page(&self.page).build()?).await?) + } + + /// Get the next page of results + pub async fn next<'a, C>(&mut self, connection: &'a C) -> Result, crate::Error> + where + C: GeekConnection + 'a, + { + self.page.next(); + if self.page.max() < self.page.page { + return Err(crate::Error::PaginationError( + "Cannot go to next page".to_string(), + )); + } + self.get(connection).await + } + + /// Get the previous page of results + pub async fn prev<'a, C>(&mut self, connection: &'a C) -> Result, crate::Error> + where + C: GeekConnection + 'a, + { + if self.page.page == u32::MAX || self.page.page == 0 { + return Err(crate::Error::PaginationError( + "Cannot go to previous page".to_string(), + )); + } + self.page.prev(); + self.get(connection).await + } +} + +impl Default for Pagination +where + T: TableBuilder + + QueryBuilderTrait + + serde::Serialize + + for<'de> serde::Deserialize<'de> + + Sized, +{ + fn default() -> Self { + Self { + phantom: std::marker::PhantomData, + page: Page { + page: u32::MAX, + ..Default::default() + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 62946c5..bb3b4f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,9 @@ pub mod prelude { pub use geekorm_core::builder::columntypes::{ColumnType, ColumnTypeOptions}; pub use geekorm_core::builder::table::Table as BuilderTable; #[cfg(feature = "pagination")] - pub use geekorm_core::queries::pages::Pagination; + pub use geekorm_core::queries::pages::Page; + #[cfg(feature = "pagination")] + pub use geekorm_core::queries::pagination::Pagination; // Keys Modules pub use geekorm_core::builder::keys::foreign::{ForeignKey, ForeignKeyInteger}; From ef8e6f2eed64dfe8b9812cbad5a92848cfcf61c1 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Fri, 10 Jan 2025 23:38:20 +0000 Subject: [PATCH 2/3] fix(examples): Update Pagination example --- examples/pagination.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/pagination.rs b/examples/pagination.rs index e9ffc08..ca9dea8 100644 --- a/examples/pagination.rs +++ b/examples/pagination.rs @@ -24,12 +24,9 @@ async fn main() -> Result<()> { let mut page = Projects::paginate(&connection).await?; - /// Get the first page of projects + // Get the first page of projects let mut projects = page.next(&connection).await?; assert_eq!(page.page(), 0); - for project in &projects { - println!("Project: {}", project.name); - } projects = page.next(&connection).await?; assert_eq!(projects.len(), 100); From 73d9467b03b901312d80f5c5cc2e081143fb0595 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Fri, 10 Jan 2025 23:39:12 +0000 Subject: [PATCH 3/3] fix(examples): Move to all API --- examples/turso-libsql/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/turso-libsql/src/main.rs b/examples/turso-libsql/src/main.rs index 3c508cc..ee82e2e 100644 --- a/examples/turso-libsql/src/main.rs +++ b/examples/turso-libsql/src/main.rs @@ -53,7 +53,7 @@ async fn main() -> Result<()> { println!("Number of projects: {}\n", count); // Query all projects - let all_projects = Projects::fetch_all(&conn).await?; + let all_projects = Projects::all(&conn).await?; for project in all_projects { println!(