From c56e4ddce53b8bf0b251b0ce57017fcc37974793 Mon Sep 17 00:00:00 2001 From: Chris Mitchell Date: Tue, 21 Apr 2026 09:48:05 -0400 Subject: [PATCH] Query poc --- gen-models/src/block_group.rs | 136 ++++++++++++++++++++---- gen-models/src/path.rs | 116 +++++++++++++++++++- gen-models/src/sample.rs | 128 +++++++++++++++++++--- gen-models/src/sample_lineage.rs | 176 ++++++++++++++++++++++++++++--- gen-models/src/traits.rs | 164 +++++++++++++++++++++++++++- 5 files changed, 660 insertions(+), 60 deletions(-) diff --git a/gen-models/src/block_group.rs b/gen-models/src/block_group.rs index 06d09d9f..e6e8217c 100644 --- a/gen-models/src/block_group.rs +++ b/gen-models/src/block_group.rs @@ -128,6 +128,56 @@ pub struct NewBlockGroup<'a> { pub is_default: bool, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct BlockGroupSelect<'a> { + pub collection_name: Option<&'a str>, + pub sample_name: Option<&'a str>, + pub name: Option<&'a str>, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +impl ModelSelect for BlockGroupSelect<'_> { + fn source_clause(&self) -> String { + "block_groups".to_string() + } + + fn filters(&self) -> Vec { + let mut filters = vec![]; + + if let Some(collection_name) = self.collection_name { + filters.push(SqlFilter::new( + "collection_name = ?", + vec![SQLValue::from(collection_name.to_string())], + )); + } + + if let Some(sample_name) = self.sample_name { + filters.push(SqlFilter::new( + "sample_name = ?", + vec![SQLValue::from(sample_name.to_string())], + )); + } + + if let Some(name) = self.name { + filters.push(SqlFilter::new( + "name = ?", + vec![SQLValue::from(name.to_string())], + )); + } + + filters + } + + fn order_by(&self) -> &[(String, Direction)] { + &self.order_by + } + + fn page(&self) -> PageRequest { + self.page + } +} + #[derive(Clone, Debug)] pub struct PathChange { pub block_group_id: HashId, @@ -194,6 +244,10 @@ impl<'a> PathCache<'a> { } impl BlockGroup { + pub fn select(conn: &GraphConnection, select: &BlockGroupSelect<'_>) -> Vec { + ::select(conn, select) + } + pub fn create(conn: &GraphConnection, new_block_group: NewBlockGroup<'_>) -> BlockGroup { Sample::get_or_create(conn, new_block_group.sample_name); let hash = BlockGroup::get_id( @@ -328,12 +382,18 @@ impl BlockGroup { group_name: &str, parent_samples: Vec, ) -> Result, QueryError> { - let existing_block_groups = BlockGroup::query( + let existing_block_groups = BlockGroup::select( conn, - "select * from block_groups - where collection_name = ?1 AND sample_name = ?2 AND name = ?3 - order by created_on, id", - params![collection_name, sample_name, group_name], + &BlockGroupSelect { + collection_name: Some(collection_name), + sample_name: Some(sample_name), + name: Some(group_name), + order_by: vec![ + ("created_on".to_string(), Direction::Asc), + ("id".to_string(), Direction::Asc), + ], + ..Default::default() + }, ); if !existing_block_groups.is_empty() { @@ -1010,12 +1070,19 @@ impl BlockGroup { } pub fn get_current_path(conn: &GraphConnection, block_group_id: &HashId) -> Path { - let paths = Path::query( + Path::select( conn, - "SELECT * FROM paths WHERE block_group_id = ?1 ORDER BY created_on DESC", - params![block_group_id], - ); - paths[0].clone() + &crate::path::PathSelect { + block_group_id: Some(block_group_id), + page: PageRequest::first(1), + order_by: vec![ + ("paths.created_on".to_string(), Direction::Desc), + ("paths.id".to_string(), Direction::Desc), + ], + ..Default::default() + }, + )[0] + .clone() } pub fn get_path_by_name( @@ -1023,19 +1090,21 @@ impl BlockGroup { block_group_id: &HashId, path_name: &str, ) -> Option { - let paths = Path::query( + Path::select( conn, - "SELECT * FROM paths WHERE block_group_id = ?1 ORDER BY created_on DESC", - params![block_group_id], - ); - - for path in &paths { - if path.name == path_name { - return Some(path.clone()); - } - } - - None + &crate::path::PathSelect { + block_group_id: Some(block_group_id), + name: Some(path_name), + page: PageRequest::first(1), + order_by: vec![ + ("paths.created_on".to_string(), Direction::Desc), + ("paths.id".to_string(), Direction::Desc), + ], + ..Default::default() + }, + ) + .into_iter() + .next() } #[allow(clippy::too_many_arguments)] @@ -1321,6 +1390,29 @@ mod tests { assert_eq!(block_group, deserialized); } + #[test] + fn test_search_supports_sort_and_pagination() { + let conn = &get_connection(None).unwrap(); + Collection::create(conn, "test"); + + let alpha = create_bg(conn, "test", "sample-a", "alpha"); + let beta = create_bg(conn, "test", "sample-a", "beta"); + let gamma = create_bg(conn, "test", "sample-b", "gamma"); + + let matches = BlockGroup::select( + conn, + &BlockGroupSelect { + collection_name: Some("test"), + page: PageRequest::new(Some(2), 1), + order_by: vec![("created_on".to_string(), Direction::Asc)], + ..Default::default() + }, + ); + + assert_eq!(matches, vec![beta, gamma]); + assert_eq!(alpha.name, "alpha"); + } + #[test] fn test_capnp_deserialization_defaults_missing_parent_to_none() { let created_on = Utc::now().timestamp_nanos_opt().unwrap(); diff --git a/gen-models/src/path.rs b/gen-models/src/path.rs index 7380cacc..a579823b 100644 --- a/gen-models/src/path.rs +++ b/gen-models/src/path.rs @@ -70,6 +70,68 @@ pub struct PathData { pub block_group_id: HashId, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PathSelect<'a> { + pub collection_name: Option<&'a str>, + pub sample_name: Option<&'a str>, + pub block_group_id: Option<&'a HashId>, + pub name: Option<&'a str>, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +impl ModelSelect for PathSelect<'_> { + fn source_clause(&self) -> String { + "paths LEFT JOIN block_groups ON paths.block_group_id = block_groups.id".to_string() + } + + fn select_clause(&self) -> &'static str { + "paths.*" + } + + fn filters(&self) -> Vec { + let mut filters = vec![]; + + if let Some(collection_name) = self.collection_name { + filters.push(SqlFilter::new( + "block_groups.collection_name = ?", + vec![SQLValue::from(collection_name.to_string())], + )); + } + + if let Some(sample_name) = self.sample_name { + filters.push(SqlFilter::new( + "block_groups.sample_name = ?", + vec![SQLValue::from(sample_name.to_string())], + )); + } + + if let Some(block_group_id) = self.block_group_id { + filters.push(SqlFilter::new( + "paths.block_group_id = ?", + vec![SQLValue::from(*block_group_id)], + )); + } + + if let Some(name) = self.name { + filters.push(SqlFilter::new( + "paths.name = ?", + vec![SQLValue::from(name.to_string())], + )); + } + + filters + } + + fn order_by(&self) -> &[(String, Direction)] { + &self.order_by + } + + fn page(&self) -> PageRequest { + self.page + } +} + // interesting gist here: https://gist.github.com/mbhall88/cd900add6335c96127efea0e0f6a9f48, see if we // can expand this to ambiguous bases/keep case pub fn revcomp(seq: &str) -> String { @@ -227,9 +289,19 @@ impl Path { Path::get(conn, "select * from paths where id = ?1;", params![path_id]).unwrap() } + pub fn select(conn: &GraphConnection, select: &PathSelect<'_>) -> Vec { + ::select(conn, select) + } + pub fn query_for_collection(conn: &GraphConnection, collection_name: &str) -> Vec { - let query = "SELECT * FROM paths JOIN block_groups ON paths.block_group_id = block_groups.id WHERE block_groups.collection_name = ?1"; - Path::query(conn, query, params![collection_name]) + Path::select( + conn, + &PathSelect { + collection_name: Some(collection_name), + order_by: vec![("paths.created_on".to_string(), Direction::Desc)], + ..Default::default() + }, + ) } pub fn query_for_collection_and_sample( @@ -237,8 +309,15 @@ impl Path { collection_name: &str, sample_name: &str, ) -> Vec { - let query = "SELECT * FROM paths JOIN block_groups ON paths.block_group_id = block_groups.id WHERE block_groups.collection_name = ?1 AND block_groups.sample_name = ?2"; - Path::query(conn, query, params![collection_name, sample_name]) + Path::select( + conn, + &PathSelect { + collection_name: Some(collection_name), + sample_name: Some(sample_name), + order_by: vec![("paths.created_on".to_string(), Direction::Desc)], + ..Default::default() + }, + ) } pub fn sequence(&self, conn: &GraphConnection) -> String { @@ -849,7 +928,7 @@ mod tests { block_group::{BlockGroup, NewBlockGroup}, block_group_edge::BlockGroupEdgeData, collection::Collection, - test_helpers::get_connection, + test_helpers::{get_connection, setup_block_group}, }; fn create_test_block_group(conn: &GraphConnection) -> BlockGroup { @@ -881,6 +960,33 @@ mod tests { assert_eq!(path, deserialized); } + #[test] + fn test_search_supports_sort_and_pagination() { + let conn = &get_connection(None).unwrap(); + let (block_group_id, seed_path) = setup_block_group(conn); + let edge_ids = PathEdge::edges_for_path(conn, &seed_path.id) + .into_iter() + .map(|edge| edge.id) + .collect::>(); + + let alpha = Path::create(conn, "alpha", &block_group_id, &edge_ids); + let beta = Path::create(conn, "beta", &block_group_id, &edge_ids); + let gamma = Path::create(conn, "gamma", &block_group_id, &edge_ids); + + let matches = Path::select( + conn, + &PathSelect { + collection_name: Some("test"), + page: PageRequest::new(Some(2), 1), + order_by: vec![("paths.created_on".to_string(), Direction::Asc)], + ..Default::default() + }, + ); + + assert_eq!(matches, vec![alpha, beta]); + assert_eq!(gamma.name, "gamma"); + } + #[test] fn test_path_delete() { let conn = &get_connection(None).unwrap(); diff --git a/gen-models/src/sample.rs b/gen-models/src/sample.rs index 516ef478..09e785d1 100644 --- a/gen-models/src/sample.rs +++ b/gen-models/src/sample.rs @@ -6,8 +6,12 @@ use rusqlite::{Result as SQLResult, Row, params, types::Value as SQLValue}; use serde::{Deserialize, Serialize}; use crate::{ - block_group::BlockGroup, db::GraphConnection, errors::SampleError, gen_models_capnp::sample, - sample_lineage::SampleLineage, traits::Query, + block_group::BlockGroup, + db::GraphConnection, + errors::SampleError, + gen_models_capnp::sample, + sample_lineage::SampleLineage, + traits::{Direction, ModelSelect, PageRequest, Query, QuerySelect, SqlFilter}, }; #[derive(Debug, Deserialize, Serialize, PartialEq)] @@ -15,6 +19,45 @@ pub struct Sample { pub name: String, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SampleSelect<'a> { + pub name_contains: Option<&'a str>, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SampleNameSelect<'a> { + pub name: &'a str, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +impl ModelSelect for SampleSelect<'_> { + fn source_clause(&self) -> String { + "samples".to_string() + } + + fn filters(&self) -> Vec { + let mut filters = vec![]; + if let Some(name_contains) = self.name_contains { + filters.push(SqlFilter::new( + "instr(lower(name), lower(?)) > 0", + vec![SQLValue::from(name_contains.to_string())], + )); + } + filters + } + + fn order_by(&self) -> &[(String, Direction)] { + &self.order_by + } + + fn page(&self) -> PageRequest { + self.page + } +} + impl<'a> Capnp<'a> for Sample { type Builder = sample::Builder<'a>; type Reader = sample::Reader<'a>; @@ -180,10 +223,14 @@ impl Sample { collection_name: &str, sample_name: &str, ) -> Vec { - BlockGroup::query( + BlockGroup::select( conn, - "select * from block_groups where collection_name = ?1 AND sample_name = ?2;", - params![collection_name, sample_name], + &crate::block_group::BlockGroupSelect { + collection_name: Some(collection_name), + sample_name: Some(sample_name), + order_by: vec![("created_on".to_string(), Direction::Asc)], + ..Default::default() + }, ) } @@ -200,13 +247,18 @@ impl Sample { ) } - pub fn search_name(conn: &GraphConnection, name: &str) -> Vec { - Sample::query( + pub fn select(conn: &GraphConnection, select: &SampleSelect<'_>) -> Vec { + ::select(conn, select) + } + + pub fn select_name(conn: &GraphConnection, select: &SampleNameSelect<'_>) -> Vec { + Sample::select( conn, - "select * from samples - where instr(lower(name), lower(?1)) > 0 - order by name;", - rusqlite::params!(name), + &SampleSelect { + name_contains: Some(select.name), + page: select.page, + order_by: select.order_by.clone(), + }, ) } } @@ -260,12 +312,56 @@ mod tests { Sample::create(conn, sample).unwrap(); } - let matches = Sample::search_name(conn, "FoO") - .into_iter() - .map(|sample| sample.name) - .collect::>(); + let matches = Sample::select_name( + conn, + &SampleNameSelect { + name: "FoO", + order_by: vec![("name".to_string(), Direction::CaseInsensitiveAsc)], + ..Default::default() + }, + ) + .into_iter() + .map(|sample| sample.name) + .collect::>(); + + assert_eq!(matches, vec!["BarFooBaz", "foo", "QuxFood"]); + + let limited_matches = Sample::select_name( + conn, + &SampleNameSelect { + name: "FoO", + page: PageRequest::first(2), + order_by: vec![("name".to_string(), Direction::CaseInsensitiveDesc)], + }, + ) + .into_iter() + .map(|sample| sample.name) + .collect::>(); + + assert_eq!(limited_matches, vec!["QuxFood", "foo"]); + } + + #[test] + fn test_search_supports_sort_and_pagination() { + let conn = &get_connection(None).unwrap(); + + for sample in ["alpha", "BarFooBaz", "foo", "QuxFood", "zzz"] { + Sample::create(conn, sample).unwrap(); + } + + let matches = Sample::select( + conn, + &SampleSelect { + name_contains: Some("o"), + page: PageRequest::new(Some(2), 1), + order_by: vec![("name".to_string(), Direction::CaseInsensitiveDesc)], + }, + ) + .into_iter() + .map(|sample| sample.name) + .collect::>(); - assert_eq!(matches, vec!["BarFooBaz", "QuxFood", "foo"]); + assert_eq!(matches, vec!["foo", "BarFooBaz"]); } #[test] diff --git a/gen-models/src/sample_lineage.rs b/gen-models/src/sample_lineage.rs index f041d9fc..67078a35 100644 --- a/gen-models/src/sample_lineage.rs +++ b/gen-models/src/sample_lineage.rs @@ -1,9 +1,12 @@ use gen_core::traits::Capnp; -use rusqlite::{Result as SQLResult, Row, params}; +use rusqlite::{Result as SQLResult, Row, params, types::Value as SQLValue}; use serde::{Deserialize, Serialize}; use crate::{ - db::GraphConnection, gen_models_capnp::sample_lineage, lineage::SqlLineage, traits::Query, + db::GraphConnection, + gen_models_capnp::sample_lineage, + lineage::SqlLineage, + traits::{Direction, ModelSelect, PageRequest, Query, QuerySelect, SqlFilter}, }; #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] @@ -12,6 +15,66 @@ pub struct SampleLineage { pub child_sample_name: String, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SampleLineageSelect<'a> { + pub name_contains: Option<&'a str>, + pub parent_sample_name: Option<&'a str>, + pub child_sample_name: Option<&'a str>, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SampleLineageNameSelect<'a> { + pub name: &'a str, + pub page: PageRequest, + pub order_by: Vec<(String, Direction)>, +} + +impl ModelSelect for SampleLineageSelect<'_> { + fn source_clause(&self) -> String { + "sample_lineage".to_string() + } + + fn filters(&self) -> Vec { + let mut filters = vec![]; + + if let Some(name_contains) = self.name_contains { + filters.push(SqlFilter::new( + "(instr(lower(parent_sample_name), lower(?)) > 0 OR instr(lower(child_sample_name), lower(?)) > 0)", + vec![ + SQLValue::from(name_contains.to_string()), + SQLValue::from(name_contains.to_string()), + ], + )); + } + + if let Some(parent_sample_name) = self.parent_sample_name { + filters.push(SqlFilter::new( + "parent_sample_name = ?", + vec![SQLValue::from(parent_sample_name.to_string())], + )); + } + + if let Some(child_sample_name) = self.child_sample_name { + filters.push(SqlFilter::new( + "child_sample_name = ?", + vec![SQLValue::from(child_sample_name.to_string())], + )); + } + + filters + } + + fn order_by(&self) -> &[(String, Direction)] { + &self.order_by + } + + fn page(&self) -> PageRequest { + self.page + } +} + impl<'a> Capnp<'a> for SampleLineage { type Builder = sample_lineage::Builder<'a>; type Reader = sample_lineage::Reader<'a>; @@ -70,11 +133,18 @@ impl SqlLineage for SampleLineage { } impl SampleLineage { + pub fn select(conn: &GraphConnection, select: &SampleLineageSelect<'_>) -> Vec { + ::select(conn, select) + } + pub fn get_parents(conn: &GraphConnection, child_sample_name: &str) -> Vec { - SampleLineage::query( + SampleLineage::select( conn, - "SELECT * FROM sample_lineage WHERE child_sample_name = ?1 ORDER BY parent_sample_name;", - params![child_sample_name], + &SampleLineageSelect { + child_sample_name: Some(child_sample_name), + order_by: vec![("parent_sample_name".to_string(), Direction::Asc)], + ..Default::default() + }, ) .into_iter() .map(|lineage| lineage.parent_sample_name) @@ -82,24 +152,28 @@ impl SampleLineage { } pub fn get_children(conn: &GraphConnection, parent_sample_name: &str) -> Vec { - SampleLineage::query( + SampleLineage::select( conn, - "SELECT * FROM sample_lineage WHERE parent_sample_name = ?1 ORDER BY child_sample_name;", - params![parent_sample_name], + &SampleLineageSelect { + parent_sample_name: Some(parent_sample_name), + order_by: vec![("child_sample_name".to_string(), Direction::Asc)], + ..Default::default() + }, ) .into_iter() .map(|lineage| lineage.child_sample_name) .collect() } - pub fn search_name(conn: &GraphConnection, name: &str) -> Vec { - SampleLineage::query( + pub fn select_name(conn: &GraphConnection, select: &SampleLineageNameSelect<'_>) -> Vec { + SampleLineage::select( conn, - "SELECT * FROM sample_lineage - WHERE instr(lower(parent_sample_name), lower(?1)) > 0 - OR instr(lower(child_sample_name), lower(?1)) > 0 - ORDER BY parent_sample_name, child_sample_name;", - params![name], + &SampleLineageSelect { + name_contains: Some(select.name), + page: select.page, + order_by: select.order_by.clone(), + ..Default::default() + }, ) } @@ -323,7 +397,17 @@ mod tests { SampleLineage::create(&conn, "plain-parent", "plain-child").unwrap(); SampleLineage::create(&conn, "zzz", "QuxFood").unwrap(); - let matches = SampleLineage::search_name(&conn, "FoO"); + let matches = SampleLineage::select_name( + &conn, + &SampleLineageNameSelect { + name: "FoO", + order_by: vec![ + ("parent_sample_name".to_string(), Direction::Asc), + ("child_sample_name".to_string(), Direction::Asc), + ], + ..Default::default() + }, + ); assert_eq!( matches, @@ -342,5 +426,65 @@ mod tests { }, ] ); + + let limited_matches = SampleLineage::select_name( + &conn, + &SampleLineageNameSelect { + name: "FoO", + page: PageRequest::first(2), + order_by: vec![ + ("child_sample_name".to_string(), Direction::Desc), + ("parent_sample_name".to_string(), Direction::Desc), + ], + }, + ); + + assert_eq!( + limited_matches, + vec![ + SampleLineage { + parent_sample_name: "foo".to_string(), + child_sample_name: "child".to_string(), + }, + SampleLineage { + parent_sample_name: "zzz".to_string(), + child_sample_name: "QuxFood".to_string(), + }, + ] + ); + } + + #[test] + fn test_search_supports_sort_and_pagination() { + let conn = get_connection(None).unwrap(); + + for sample in ["alpha", "beta", "child-a", "child-b", "foo", "zzz"] { + Sample::get_or_create(&conn, sample); + } + + SampleLineage::create(&conn, "alpha", "child-a").unwrap(); + SampleLineage::create(&conn, "foo", "child-b").unwrap(); + SampleLineage::create(&conn, "zzz", "beta").unwrap(); + + let matches = SampleLineage::select( + &conn, + &SampleLineageSelect { + name_contains: Some("a"), + page: PageRequest::new(Some(2), 1), + order_by: vec![ + ("child_sample_name".to_string(), Direction::Desc), + ("parent_sample_name".to_string(), Direction::Desc), + ], + ..Default::default() + }, + ); + + assert_eq!( + matches, + vec![SampleLineage { + parent_sample_name: "zzz".to_string(), + child_sample_name: "beta".to_string(), + }] + ); } } diff --git a/gen-models/src/traits.rs b/gen-models/src/traits.rs index 1408c3fb..c08ff515 100644 --- a/gen-models/src/traits.rs +++ b/gen-models/src/traits.rs @@ -1,7 +1,9 @@ use std::rc::Rc; use itertools::Itertools; -use rusqlite::{Connection, Params, Result, Row, limits::Limit, params, types::Value}; +use rusqlite::{ + Connection, Params, Result, Row, limits::Limit, params, params_from_iter, types::Value, +}; /// Returns the SQLite variable parameter limit for the provided connection. pub fn sqlite_parameter_limit(conn: &Connection) -> usize { @@ -16,6 +18,105 @@ pub fn max_rows_per_batch(conn: &Connection, params_per_row: usize) -> usize { (max_params / params_per_row).max(1) } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum Direction { + #[default] + Asc, + Desc, + CaseInsensitiveAsc, + CaseInsensitiveDesc, +} + +impl Direction { + pub const fn as_sql(self) -> &'static str { + match self { + Self::Asc => "ASC", + Self::Desc => "DESC", + Self::CaseInsensitiveAsc => "ASC", + Self::CaseInsensitiveDesc => "DESC", + } + } + + pub const fn uses_case_insensitive_collation(self) -> bool { + match self { + Self::Asc | Self::Desc => false, + Self::CaseInsensitiveAsc | Self::CaseInsensitiveDesc => true, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PageRequest { + pub limit: Option, + pub offset: u32, +} + +impl PageRequest { + pub const fn new(limit: Option, offset: u32) -> Self { + Self { limit, offset } + } + + pub const fn first(limit: u32) -> Self { + Self { + limit: Some(limit), + offset: 0, + } + } + + pub const fn unbounded() -> Self { + Self { + limit: None, + offset: 0, + } + } + + pub fn append_sql(self, query: &mut String, params: &mut Vec) { + if let Some(limit) = self.limit { + query.push_str(" LIMIT ?"); + params.push(Value::from(i64::from(limit))); + } + + if self.offset > 0 { + if self.limit.is_none() { + query.push_str(" LIMIT -1"); + } + query.push_str(" OFFSET ?"); + params.push(Value::from(i64::from(self.offset))); + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SqlFilter { + pub clause: String, + pub params: Vec, +} + +impl SqlFilter { + pub fn new(clause: impl Into, params: Vec) -> Self { + Self { + clause: clause.into(), + params, + } + } +} + +pub trait ModelSelect { + fn source_clause(&self) -> String; + + fn select_clause(&self) -> &'static str { + "*" + } + + fn filters(&self) -> Vec { + vec![] + } + + fn order_by(&self) -> &[(String, Direction)]; + + fn page(&self) -> PageRequest; +} + pub trait Query { type Model; const PRIMARY_KEY: &'static str = "id"; @@ -33,11 +134,28 @@ pub trait Query { objs } + fn query_values(conn: &Connection, query: &str, params: Vec) -> Vec { + let mut stmt = conn.prepare(query).unwrap(); + let rows = stmt + .query_map(params_from_iter(params), |row| Ok(Self::process_row(row))) + .unwrap(); + let mut objs = vec![]; + for row in rows { + objs.push(row.unwrap()); + } + objs + } + fn get(conn: &Connection, query: &str, params: impl Params) -> Result { let mut stmt = conn.prepare(query).unwrap(); stmt.query_row(params, |row| Ok(Self::process_row(row))) } + fn get_values(conn: &Connection, query: &str, params: Vec) -> Result { + let mut stmt = conn.prepare(query).unwrap(); + stmt.query_row(params_from_iter(params), |row| Ok(Self::process_row(row))) + } + fn get_by_id<'a, T>(conn: &Connection, id: &'a T) -> Option where T: Clone + 'a, @@ -135,6 +253,50 @@ pub trait Query { } } +pub trait QuerySelect: Query { + fn select(conn: &Connection, select: &S) -> Vec { + let mut query = format!( + "SELECT {} FROM {}", + select.select_clause(), + select.source_clause() + ); + let mut params = vec![]; + let filters = select.filters(); + + if !filters.is_empty() { + query.push_str(" WHERE "); + for (index, filter) in filters.iter().enumerate() { + if index > 0 { + query.push_str(" AND "); + } + query.push_str(&filter.clause); + params.extend(filter.params.iter().cloned()); + } + } + + let order_by = select.order_by(); + if !order_by.is_empty() { + query.push_str(" ORDER BY "); + for (index, (column, direction)) in order_by.iter().enumerate() { + if index > 0 { + query.push_str(", "); + } + query.push_str(column); + if direction.uses_case_insensitive_collation() { + query.push_str(" COLLATE NOCASE"); + } + query.push(' '); + query.push_str(direction.as_sql()); + } + } + + select.page().append_sql(&mut query, &mut params); + Self::query_values(conn, &query, params) + } +} + +impl QuerySelect for T {} + #[cfg(test)] mod tests { use super::*;