diff --git a/model/src/connection.rs b/model/src/connection.rs index 890fc9e..fb8e637 100644 --- a/model/src/connection.rs +++ b/model/src/connection.rs @@ -19,6 +19,7 @@ pub struct Connection { #[derive(Clone, Debug, Serialize, JsonSchema)] pub struct PageInfo { pub next_cursor: Option, + pub prev_cursor: Option, } impl JsonSchema for Connection { diff --git a/model/src/crud/join.rs b/model/src/crud/join.rs index cf00f73..faba8db 100644 --- a/model/src/crud/join.rs +++ b/model/src/crud/join.rs @@ -73,29 +73,23 @@ where F: Model, { pub async fn fetch_page(&self, executor: &mut PgConnection) -> Result, Error> { - let nodes = self.fetch_all(executor).await?; - - self.select.paginate(nodes) - } - - pub async fn fetch_all(&self, executor: &mut PgConnection) -> Result, Error> { let filters = self.select.build_filters_with_foreign::()?; let select_clause = self.prepare_select_clause(); - let (statement, var_bindings) = self.select.prepare(filters, select_clause.into()); + let (statement, var_bindings) = self.select.prepare(filters, select_clause.into())?; - let result = build_query_as::(&statement, var_bindings) + let nodes = build_query_as::(&statement, var_bindings) .fetch_all(executor) .await?; - Ok(result) + self.select.paginate(nodes) } pub async fn fetch_one(&self, executor: &mut PgConnection) -> Result { let filters = self.select.build_filters_with_foreign::()?; let select_clause = self.prepare_select_clause(); - let (statement, var_bindings) = self.select.prepare(filters, select_clause.into()); + let (statement, var_bindings) = self.select.prepare(filters, select_clause.into())?; let result = build_query_as::(&statement, var_bindings) .fetch_one(executor) @@ -108,7 +102,7 @@ where let filters = self.select.build_filters_with_foreign::()?; let select_clause = self.prepare_select_clause(); - let (statement, var_bindings) = self.select.prepare(filters, select_clause.into()); + let (statement, var_bindings) = self.select.prepare(filters, select_clause.into())?; let result = build_query_as::(&statement, var_bindings) .fetch_optional(executor) diff --git a/model/src/crud/select.rs b/model/src/crud/select.rs index 59a809e..edfd3ba 100644 --- a/model/src/crud/select.rs +++ b/model/src/crud/select.rs @@ -10,10 +10,13 @@ use crate::{ use super::{join::Join, util::build_query_as}; +const DEFAULT_LIMIT: i64 = 100; + #[derive(Clone, Debug)] pub struct Select { filters: Vec, order_by: OrderBy, + cursor: Option, pub(crate) limit: Option, for_update: bool, _marker: PhantomData, @@ -27,6 +30,49 @@ pub enum OrderBy { SecondaryDesc(String), } +impl OrderBy { + fn inverse(&self) -> Self { + match self { + OrderBy::IdAsc => Self::IdDesc, + OrderBy::IdDesc => Self::IdAsc, + OrderBy::SecondaryAsc(field) => Self::SecondaryDesc(field.clone()), + OrderBy::SecondaryDesc(field) => Self::SecondaryAsc(field.clone()), + } + } + + fn to_string(&self, id_field_name: &str, for_join: bool) -> String { + match &self { + OrderBy::IdAsc => format!("{} ASC", id_field_name), + OrderBy::IdDesc => format!("{} DESC", id_field_name), + OrderBy::SecondaryAsc(field_name) => { + let field_name = if for_join { + format!("a.{}", field_name) + } else { + field_name.into() + }; + + format!("{} ASC, {} ASC", field_name, id_field_name) + } + OrderBy::SecondaryDesc(field_name) => { + let field_name = if for_join { + format!("a.{}", field_name) + } else { + field_name.into() + }; + + format!("{} DESC, {} DESC", field_name, id_field_name) + } + } + } + + fn is_ascending(&self) -> bool { + match self { + OrderBy::IdAsc | OrderBy::SecondaryAsc(_) => true, + _ => false, + } + } +} + impl Select { /// This will override the entire state of the existing Select with the parameters defined in query pub fn from_query(self, query: Query) -> Result { @@ -71,25 +117,19 @@ where T: Model + for<'a> FromRow<'a, PgRow> + Unpin + Sized + Send, { pub async fn fetch_page(&self, executor: &mut PgConnection) -> Result, Error> { - let nodes = self.fetch_all(executor).await?; - - self.paginate(nodes) - } - - pub async fn fetch_all(&self, executor: &mut PgConnection) -> Result, Error> { let filters = self.build_filters()?; - let (statement, var_bindings) = self.prepare(filters, None); + let (statement, var_bindings) = self.prepare(filters, None)?; - let result = build_query_as::(&statement, var_bindings) + let nodes = build_query_as::(&statement, var_bindings) .fetch_all(executor) .await?; - Ok(result) + self.paginate(nodes) } pub async fn fetch_one(&self, executor: &mut PgConnection) -> Result { let filters = self.build_filters()?; - let (statement, var_bindings) = self.prepare(filters, None); + let (statement, var_bindings) = self.prepare(filters, None)?; let result = build_query_as::(&statement, var_bindings) .fetch_one(executor) @@ -100,7 +140,7 @@ where pub async fn fetch_optional(&self, executor: &mut PgConnection) -> Result, Error> { let filters = self.build_filters()?; - let (statement, var_bindings) = self.prepare(filters, None); + let (statement, var_bindings) = self.prepare(filters, None)?; let result = build_query_as::(&statement, var_bindings) .fetch_optional(executor) @@ -115,51 +155,72 @@ impl Select { Self { filters: vec![], order_by: OrderBy::IdAsc, + cursor: None, limit: None, for_update: false, _marker: PhantomData::default(), } } - pub(crate) fn paginate(&self, mut nodes: Vec) -> Result, Error> { + pub(crate) fn paginate(&self, nodes: Vec) -> Result, Error> { + let mut prev_cursor = None; + + let mut page_nodes = if let Some(cursor) = &self.cursor { + let (prev, next) = split_nodes(nodes, cursor, &self.order_by)?; + + prev_cursor = prev + .iter() + .next() + .map(|n| build_cursor(n, &self.order_by)) + .transpose()?; + + next + } else { + nodes + }; + match self.limit { - Some(limit) if (nodes.len() as i64) > limit => { - let cursor_node = nodes.pop().ok_or_else(|| { + Some(limit) if (page_nodes.len() as i64) > limit => { + let cursor_node = page_nodes.pop().ok_or_else(|| { Error::internal("cursor_node should not be empty. this is a bug") })?; - let next_cursor = match &self.order_by { - OrderBy::IdAsc | OrderBy::IdDesc => Cursor { - id: cursor_node.id_field_value(), - value: None, - } - .into(), - OrderBy::SecondaryAsc(field) | OrderBy::SecondaryDesc(field) => Cursor { - id: cursor_node.id_field_value(), - value: cursor_node.field_value(field)?.into(), - } - .into(), - }; + let next_cursor = build_cursor(&cursor_node, &self.order_by)?; Ok(Connection { - nodes, - page_info: PageInfo { next_cursor }, + nodes: page_nodes, + page_info: PageInfo { + prev_cursor, + next_cursor: next_cursor.into(), + }, }) } _ => Ok(Connection { - nodes, - page_info: PageInfo { next_cursor: None }, + nodes: page_nodes, + page_info: PageInfo { + prev_cursor, + next_cursor: None, + }, }), } } + /// prepares a query statement that fetches a max size of limit * 2 + 1. + /// includes limit + 1 rows after the provided cursor and limit rows before pub(crate) fn prepare( &self, exprs: Vec, joined_select_clause: Option, - ) -> (String, Vec) { + ) -> Result<(String, Vec), Error> { let table_name = T::table_name(); + let id_field_name = T::id_field_name(); + let id_field_name = if joined_select_clause.is_some() { + format!("a.{}", id_field_name) + } else { + id_field_name + }; + let select_clause = if let Some(s) = &joined_select_clause { s.into() } else { @@ -176,72 +237,162 @@ impl Select { var_bindings.extend(bindings); } - let predicate = predicates.join(" AND "); - let where_clause = format!("WHERE {}", predicate); + let mut statement = match &self.cursor { + Some(cursor) => { + let mut inverse_predicates = predicates.clone(); - let id_field_name = T::id_field_name(); - let id_field_name = if joined_select_clause.is_some() { - format!("a.{}", id_field_name) - } else { - id_field_name - }; + let cursor_filter = build_cursor_filter::( + cursor, + &id_field_name, + &self.order_by, + joined_select_clause.is_some(), + )?; - let group_by_clause = if joined_select_clause.is_some() { - format!("GROUP BY {}", id_field_name) - } else { - "".into() - }; + let inverse_cursor_filter = build_cursor_filter::( + cursor, + &id_field_name, + &self.order_by.inverse(), + joined_select_clause.is_some(), + )?; - let order_by = match &self.order_by { - OrderBy::IdAsc => format!("{} ASC", id_field_name), - OrderBy::IdDesc => format!("{} DESC", id_field_name), - OrderBy::SecondaryAsc(field_name) => { - let field_name = if joined_select_clause.is_some() { - format!("a.{}", field_name) + let (sql, bindings) = cursor_filter.to_sql(var_bindings.len()); + predicates.push(sql); + var_bindings.extend(bindings); + + let (sql, bindings) = inverse_cursor_filter.to_sql(var_bindings.len()); + inverse_predicates.push(sql); + var_bindings.extend(bindings); + + let predicate = predicates.join(" AND "); + let where_clause = format!("WHERE {}", predicate); + + let inverse_predicate = inverse_predicates.join(" AND "); + let inverse_where_clause = format!("WHERE {}", inverse_predicate); + + let group_by_clause = if joined_select_clause.is_some() { + format!("GROUP BY {}", id_field_name) } else { - field_name.into() + "".into() }; - format!("{} ASC, {} ASC", field_name, id_field_name) + let order_by = self + .order_by + .to_string(&id_field_name, joined_select_clause.is_some()); + + let order_by_clause = format!("ORDER BY {}", order_by); + + let inverse_order_by = self + .order_by + .inverse() + .to_string(&id_field_name, joined_select_clause.is_some()); + + let inverse_order_by_clause = format!("ORDER BY {}", inverse_order_by); + + let limit = match self.limit { + Some(limit) if limit > 0 => limit, + _ => DEFAULT_LIMIT, + }; + + let limit_clause = format!("LIMIT {}", limit + 1); + + let inverse_limit_clause = format!("LIMIT {}", limit + 1); + + let next_page_query = format!( + " + {} + {} + {} + {} + {} + ", + select_clause, where_clause, group_by_clause, order_by_clause, limit_clause + ); + + let previous_page_query = format!( + " + {} + {} + {} + {} + {} + ", + select_clause, + inverse_where_clause, + group_by_clause, + inverse_order_by_clause, + inverse_limit_clause + ); + + format!( + " + WITH aggregated AS ( + ( + {} + ) + + UNION + + ( + {} + ) + ) + SELECT * + FROM aggregated + {} + ", + previous_page_query, next_page_query, order_by_clause + ) } - OrderBy::SecondaryDesc(field_name) => { - let field_name = if joined_select_clause.is_some() { - format!("a.{}", field_name) + _ => { + let predicate = predicates.join(" AND "); + let where_clause = format!("WHERE {}", predicate); + + let group_by_clause = if joined_select_clause.is_some() { + format!("GROUP BY {}", id_field_name) } else { - field_name.into() + "".into() }; - format!("{} DESC, {} DESC", field_name, id_field_name) - } - }; + let order_by = self + .order_by + .to_string(&id_field_name, joined_select_clause.is_some()); - let order_by_clause = format!("ORDER BY {}", order_by); + let order_by_clause = format!("ORDER BY {}", order_by); - let limit_clause = if let Some(limit) = self.limit { - format!("LIMIT {}", limit + 1) - } else { - "".into() - }; + let limit = match self.limit { + Some(limit) if limit > 0 => limit, + _ => DEFAULT_LIMIT, + }; - let mut statement = format!( - " - {} - {} - {} - {} - {} - ", - select_clause, where_clause, group_by_clause, order_by_clause, limit_clause - ); + let limit_clause = format!("LIMIT {}", limit + 1); + + format!( + " + {} + {} + {} + {} + {} + ", + select_clause, where_clause, group_by_clause, order_by_clause, limit_clause + ) + } + }; if self.for_update { - statement = format!("{} FOR UPDATE", statement); + statement = format!( + " + {} + FOR UPDATE + ", + statement + ); } // debugging println!("{}", statement); - (statement, var_bindings) + Ok((statement, var_bindings)) } pub(crate) fn build_filters(&self) -> Result, Error> { @@ -287,51 +438,109 @@ impl TryFrom> for Select { filters.push(filter.try_into()?); } - if let Some(cursor) = query.cursor { - let cursor_filter = match &order_by { - OrderBy::IdAsc => Filter::new().field(&id_field_name).gte(cursor.id), - OrderBy::IdDesc => Filter::new().field(&id_field_name).lte(cursor.id), - OrderBy::SecondaryAsc(secondary) => { - let secondary_value = cursor.value.ok_or_else(|| Error::bad_request("invalid cursor: a cursor containing a value referencing the sort_by field is required"))?; - Filter::new() - .field(secondary) - .gt(secondary_value.clone()) - .or() - .group( - Filter::new() - .field(secondary) - .gte(secondary_value) - .and() - .field(&id_field_name) - .gte(cursor.id), - ) - } - OrderBy::SecondaryDesc(secondary) => { - let secondary_value = cursor.value.ok_or_else(|| Error::bad_request("invalid cursor: a cursor containing a value referencing the sort_by field is required"))?; - Filter::new() - .field(secondary) - .lt(secondary_value.clone()) - .or() - .group( - Filter::new() - .field(secondary) - .lte(secondary_value) - .and() - .field(&id_field_name) - .lte(cursor.id), - ) - } - }; - - filters.push(cursor_filter); - } - Ok(Select { filters, order_by, - limit: query.limit.clone(), + cursor: query.cursor, + limit: query.limit, for_update: false, _marker: PhantomData::default(), }) } } + +fn build_cursor_filter( + cursor: &Cursor, + id_field_name: &str, + order_by: &OrderBy, + for_join: bool, +) -> Result { + let filter = match order_by { + OrderBy::IdAsc => Filter::new().field(id_field_name).gte(cursor.id), + OrderBy::IdDesc => Filter::new().field(id_field_name).lte(cursor.id), + OrderBy::SecondaryAsc(secondary) => { + let secondary = if for_join { + format!("a.{}", secondary) + } else { + secondary.into() + }; + + let secondary_value = cursor.value.clone().ok_or_else(|| Error::bad_request("invalid cursor: a cursor containing a value referencing the sort_by field is required"))?; + Filter::new() + .field(&secondary) + .gt(secondary_value.clone()) + .or() + .group( + Filter::new() + .field(&secondary) + .gte(secondary_value) + .and() + .field(id_field_name) + .gte(cursor.id), + ) + } + OrderBy::SecondaryDesc(secondary) => { + let secondary = if for_join { + format!("a.{}", secondary) + } else { + secondary.into() + }; + + let secondary_value = cursor.value.clone().ok_or_else(|| Error::bad_request("invalid cursor: a cursor containing a value referencing the sort_by field is required"))?; + Filter::new() + .field(&secondary) + .lt(secondary_value.clone()) + .or() + .group( + Filter::new() + .field(&secondary) + .lte(secondary_value) + .and() + .field(id_field_name) + .lte(cursor.id), + ) + } + }; + + filter.build::() +} + +fn build_cursor(node: &T, order_by: &OrderBy) -> Result { + let cursor = match order_by { + OrderBy::IdAsc | OrderBy::IdDesc => Cursor { + id: node.id_field_value(), + value: None, + } + .into(), + OrderBy::SecondaryAsc(field) | OrderBy::SecondaryDesc(field) => Cursor { + id: node.id_field_value(), + value: node.field_value(field)?.into(), + } + .into(), + }; + + Ok(cursor) +} + +fn split_nodes( + nodes: Vec, + cursor: &Cursor, + order_by: &OrderBy, +) -> Result<(Vec, Vec), Error> { + let mut prev = vec![]; + let mut next = vec![]; + + for node in nodes.into_iter() { + let c = build_cursor(&node, order_by)?; + + if order_by.is_ascending() && &c >= cursor { + next.push(node) + } else if !order_by.is_ascending() && &c <= cursor { + next.push(node) + } else { + prev.push(node) + } + } + + Ok((prev, next)) +} diff --git a/model/src/cursor.rs b/model/src/cursor.rs index 331609a..86246d0 100644 --- a/model/src/cursor.rs +++ b/model/src/cursor.rs @@ -7,15 +7,24 @@ use schemars::{ JsonSchema, }; use serde::{Serialize, Serializer}; -use std::borrow::Cow; +use std::{borrow::Cow, cmp::Ordering}; use uuid::Uuid; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Cursor { pub value: Option, pub id: Uuid, } +impl PartialOrd for Cursor { + fn partial_cmp(&self, other: &Self) -> Option { + match self.value.partial_cmp(&other.value) { + Some(Ordering::Equal) | None => self.id.partial_cmp(&other.id), + Some(ordering) => Some(ordering), + } + } +} + impl Serialize for Cursor { fn serialize(&self, serializer: S) -> Result where diff --git a/model/src/field_value.rs b/model/src/field_value.rs index da5765f..699d938 100644 --- a/model/src/field_value.rs +++ b/model/src/field_value.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use chrono::{DateTime, FixedOffset, NaiveDate, Utc}; use rust_decimal::Decimal; use serde::Serialize; @@ -22,6 +24,74 @@ pub enum FieldValue { Enum(Option), } +impl PartialOrd for FieldValue { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Uuid(a), Self::Uuid(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Bool(a), Self::Bool(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Int(a), Self::Int(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Int32(a), Self::Int32(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Float(a), Self::Float(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Decimal(a), Self::Decimal(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::String(a), Self::String(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Date(a), Self::Date(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::DateTime(a), Self::DateTime(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + (Self::Enum(a), Self::Enum(b)) => match (a, b) { + (Some(a), Some(b)) => a.partial_cmp(b), + (None, Some(_)) => Some(Ordering::Less), + (Some(_), None) => Some(Ordering::Greater), + (None, None) => Some(Ordering::Equal), + }, + _ => None, + } + } +} + impl ToString for FieldValue { fn to_string(&self) -> String { match self { diff --git a/model/tests/docker-compose.yml b/model/tests/docker-compose.yml new file mode 100644 index 0000000..08b9b43 --- /dev/null +++ b/model/tests/docker-compose.yml @@ -0,0 +1,19 @@ +services: + postgres-opticon: + image: postgres:16 + environment: + POSTGRES_DB: "model" + POSTGRES_USER: "model" + POSTGRES_PASSWORD: "model" + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/model/postgresql/data + command: | + postgres + -c wal_level=logical + -c max_wal_senders=5 + -c max_replication_slots=5 +volumes: + postgres_data: + driver: local \ No newline at end of file diff --git a/model/tests/dummy_records.json b/model/tests/dummy_records.json new file mode 100644 index 0000000..e4d9292 --- /dev/null +++ b/model/tests/dummy_records.json @@ -0,0 +1,82 @@ +[ + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d85", + "name": "Abe", + "age": 22 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d86", + "name": "Bob", + "age": 21 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d87", + "name": "Charlie", + "age": 26 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d88", + "name": "Dominic", + "age": 24 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d89", + "name": "Edgar", + "age": 21 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d90", + "name": "Frankie", + "age": 21 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d91", + "name": "Geraldine", + "age": 23 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d92", + "name": "Hollie", + "age": 27 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d93", + "name": "Ingrid", + "age": 26 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d94", + "name": "Jordie", + "age": 27 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d95", + "name": "Kendra", + "age": 30 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d96", + "name": "Kerry", + "age": 30 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d97", + "name": "Lewis", + "age": 31 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d98", + "name": "Mark", + "age": 28 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8d99", + "name": "Noble", + "age": 29 + }, + { + "id": "069f0ae7-e354-4099-83c7-4c20616f8e00", + "name": "Octavia", + "age": 22 + } +] diff --git a/model/tests/pagination.rs b/model/tests/pagination.rs new file mode 100644 index 0000000..ca0ff8c --- /dev/null +++ b/model/tests/pagination.rs @@ -0,0 +1,153 @@ +use std::{fs::File, io::BufReader, path::Path, str::FromStr}; + +use model::{Crud, Cursor, Filter, Migration, Model, Query, Sort}; +use serde::{Deserialize, Serialize}; +use sqlx::{prelude::FromRow, PgPool, Postgres, Transaction}; +use uuid::Uuid; + +#[derive(Clone, Debug, Serialize, Deserialize, Model, FromRow)] +#[model(table_name = "dummy")] +struct Dummy { + #[model(id, primary_key)] + id: Uuid, + name: Option, + age: Option, +} + +async fn create_db_pool() -> PgPool { + PgPool::connect("postgresql://model:model@localhost:5432/model") + .await + .unwrap() +} + +async fn setup_tables(tx: &mut Transaction<'_, Postgres>) { + Dummy::migrate(tx).await.unwrap(); +} + +async fn insert_records(tx: &mut Transaction<'_, Postgres>) { + let records = read_records(); + + for record in records.into_iter() { + record.create().execute(tx).await.unwrap(); + } +} + +fn read_records() -> Vec { + let path = Path::new("./tests/dummy_records.json"); + + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + serde_json::from_reader(reader).unwrap() +} + +#[tokio::test] +async fn test_pagination() { + let pool = create_db_pool().await; + + let mut tx = pool.begin().await.unwrap(); + + setup_tables(&mut tx).await; + insert_records(&mut tx).await; + + let mut query = Query::new(); + query.limit = 2.into(); + query.filter = Filter::new() + .field("age") + .gte(28) + .build::() + .unwrap() + .into(); + query.sort = Sort { + field: "age".into(), + direction: model::SortDirection::Ascending, + } + .into(); + + let connection = Dummy::select() + .from_query(query.clone()) + .unwrap() + .fetch_page(&mut tx) + .await + .unwrap(); + + assert_eq!(connection.page_info.prev_cursor, None); + assert_eq!( + connection.page_info.next_cursor, + Some(Cursor { + id: Uuid::from_str("069f0ae7-e354-4099-83c7-4c20616f8d95").unwrap(), + value: Some(model::FieldValue::Int(30.into())), + }) + ); + + let mut nodes = connection.nodes.into_iter(); + let node = nodes.next().unwrap(); + + assert_eq!(node.name, Some("Mark".to_string())); + + let node = nodes.next().unwrap(); + + assert_eq!(node.name, Some("Noble".to_string())); + + query.cursor = connection.page_info.next_cursor; + + let connection = Dummy::select() + .from_query(query.clone()) + .unwrap() + .fetch_page(&mut tx) + .await + .unwrap(); + + assert_eq!( + connection.page_info.prev_cursor, + Some(Cursor { + id: Uuid::from_str("069f0ae7-e354-4099-83c7-4c20616f8d98").unwrap(), + value: Some(model::FieldValue::Int(28.into())), + }) + ); + assert_eq!( + connection.page_info.next_cursor, + Some(Cursor { + id: Uuid::from_str("069f0ae7-e354-4099-83c7-4c20616f8d97").unwrap(), + value: Some(model::FieldValue::Int(31.into())), + }) + ); + + let mut nodes = connection.nodes.into_iter(); + let node = nodes.next().unwrap(); + + assert_eq!(node.name, Some("Kendra".to_string())); + + let node = nodes.next().unwrap(); + + assert_eq!(node.name, Some("Kerry".to_string())); + + query.cursor = connection.page_info.next_cursor; + + let connection = Dummy::select() + .from_query(query.clone()) + .unwrap() + .fetch_page(&mut tx) + .await + .unwrap(); + + assert_eq!( + connection.page_info.prev_cursor, + Some(Cursor { + id: Uuid::from_str("069f0ae7-e354-4099-83c7-4c20616f8d95").unwrap(), + value: Some(model::FieldValue::Int(30.into())), + }) + ); + assert_eq!(connection.page_info.next_cursor, None); + + let mut nodes = connection.nodes.into_iter(); + let node = nodes.next().unwrap(); + + assert_eq!(node.name, Some("Lewis".to_string())); + + let next = nodes.next(); + + assert!(next.is_none()); + + tx.rollback().await.unwrap(); +}