From eee4744b3215ea5918d403383639c541a76c336e Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Sat, 11 Jan 2025 00:57:28 +0000 Subject: [PATCH 1/3] feat(core): Add Filter feature --- examples/filter.rs | 62 ++++++++++++++++++++++++++++++++ geekorm-core/src/backends/mod.rs | 34 ++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 examples/filter.rs diff --git a/examples/filter.rs b/examples/filter.rs new file mode 100644 index 0000000..2793468 --- /dev/null +++ b/examples/filter.rs @@ -0,0 +1,62 @@ +//! # Filter 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?; + + // Filter projects by name, multiple filters using `and` + let results = + Projects::filter(&connection, vec![("name", "serde"), ("name", "geekorm")]).await?; + assert_eq!(results.len(), 2); + println!("Results: {:?}", results); + + // Filter out projects by name + let filter_out = + Projects::filter(&connection, vec![("!name", "serde"), ("!name", "sqlx")]).await?; + println!("Filtered out results:"); + assert_eq!(filter_out.len(), 5); + for project in filter_out { + println!("Project: {:?}", project); + } + + Ok(()) +} + +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?; + + let project_names = vec![ + "serde", "tokio", "actix", "rocket", "geekorm", "sqlx", "libsql", + ]; + + for pname in project_names { + let mut prj = Projects::new(pname); + prj.save(&connection).await?; + } + + let total = Projects::total(&connection).await?; + assert_eq!(total, 7); + + Ok(connection) +} diff --git a/geekorm-core/src/backends/mod.rs b/geekorm-core/src/backends/mod.rs index 45795b2..0909310 100644 --- a/geekorm-core/src/backends/mod.rs +++ b/geekorm-core/src/backends/mod.rs @@ -172,6 +172,40 @@ where #[allow(async_fn_in_trait, unused_variables)] async fn fetch(&mut self, connection: &'a C) -> Result<(), crate::Error>; + /// Filter the rows in the table based on specific criteria passed as a tuple of (&str, Value). + /// + /// You can use prefix operators to define the type of comparison to use: + /// + /// - `=`: Equal + /// - `~`: Like + /// - `!`: Not equal + /// + /// If no prefix is used, the default comparison is equal. + #[allow(async_fn_in_trait, unused_variables)] + async fn filter( + connection: &'a C, + fields: Vec<(&str, impl Into)>, + ) -> Result, crate::Error> { + let mut query = Self::query_select().table(Self::table()); + + for (field, value) in fields { + if field.starts_with("=") { + let field = &field[1..]; + query = query.where_eq(field, value.into()); + } else if field.starts_with("~") { + let field = &field[1..]; + query = query.where_like(field, value.into()); + } else if field.starts_with("!") { + let field = &field[1..]; + query = query.where_ne(field, value.into()); + } else { + // Default to WHERE field = value with an OR operator + query = query.where_eq(field, value.into()).or(); + } + } + Self::query(connection, query.build()?).await + } + /// Fetch all rows from the database #[deprecated( since = "0.8.4", From 06ace504a0ea2c2aa9189e6ab188979a09a45421 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Sat, 11 Jan 2025 00:58:34 +0000 Subject: [PATCH 2/3] feat(core): Update to Values to solve various problems --- geekorm-core/src/backends/libsql.rs | 36 ++++++++++++++------------ geekorm-core/src/builder/table.rs | 8 ++---- geekorm-core/src/builder/values/mod.rs | 26 +++++++------------ geekorm-core/src/error.rs | 6 +++++ geekorm-core/src/queries/builder.rs | 17 ++++++++++-- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/geekorm-core/src/backends/libsql.rs b/geekorm-core/src/backends/libsql.rs index d6b4019..bda249f 100644 --- a/geekorm-core/src/backends/libsql.rs +++ b/geekorm-core/src/backends/libsql.rs @@ -65,7 +65,7 @@ impl GeekConnection for libsql::Connection { connection .execute(query.to_str(), ()) .await - .map_err(|e| crate::Error::LibSQLError(e.to_string()))?; + .map_err(|e| crate::Error::QuerySyntaxError(e.to_string(), query.to_string()))?; Ok(()) } @@ -80,7 +80,7 @@ impl GeekConnection for libsql::Connection { let mut statement = connection .prepare(query.to_str()) .await - .map_err(|e| crate::Error::LibSQLError(format!("Error preparing query: `{}`", e)))?; + .map_err(|e| crate::Error::QuerySyntaxError(e.to_string(), query.to_string()))?; let parameters: Vec = match convert_values(&query) { Ok(parameters) => parameters, @@ -149,7 +149,10 @@ impl GeekConnection for libsql::Connection { debug!("Error preparing query: `{}`", query.to_str()); debug!("Parameters :: {:?}", query.parameters); } - return Err(crate::Error::LibSQLError(e.to_string())); + return Err(crate::Error::QuerySyntaxError( + e.to_string(), + query.to_string(), + )); } }; @@ -229,7 +232,10 @@ impl GeekConnection for libsql::Connection { debug!("Error preparing query: `{}`", query.to_str()); debug!("Parameters :: {:?}", query.parameters); } - return Err(crate::Error::LibSQLError(e.to_string())); + return Err(crate::Error::QuerySyntaxError( + e.to_string(), + query.to_string(), + )); } }; // Convert the values to libsql::Value @@ -310,7 +316,7 @@ impl GeekConnection for libsql::Connection { { error!("Error executing query: `{}`", e); } - crate::Error::LibSQLError(e.to_string()) + crate::Error::QuerySyntaxError(e.to_string(), query.to_string()) })?; Ok(()) } @@ -334,7 +340,10 @@ impl GeekConnection for libsql::Connection { { error!("Error preparing query: `{}`", query.to_str()); } - return Err(crate::Error::LibSQLError(e.to_string())); + return Err(crate::Error::QuerySyntaxError( + e.to_string(), + query.to_string(), + )); } }; @@ -487,19 +496,12 @@ fn convert_values(query: &crate::Query) -> Result, crate::Err let mut parameters: Vec = Vec::new(); // TODO(geekmasher): This is awful, need to refactor this - let values: Values = match query.query_type { - QueryType::Insert | QueryType::Update => query.parameters.clone(), - _ => query.values.clone(), + let values: &Values = match query.query_type { + QueryType::Insert | QueryType::Update => &query.parameters, + _ => &query.values, }; - for column_name in &values.order { - let value = values - .get(&column_name.to_string()) - .ok_or(crate::Error::LibSQLError(format!( - "Error getting value for column - {}", - column_name - )))?; - + for (column_name, value) in &values.values { // Check if the column exists in the table // The column_name could be in another table not part of the query (joins) if let Some(column) = query.table.columns.get(column_name.as_str()) { diff --git a/geekorm-core/src/builder/table.rs b/geekorm-core/src/builder/table.rs index f1097f0..5c21a80 100644 --- a/geekorm-core/src/builder/table.rs +++ b/geekorm-core/src/builder/table.rs @@ -153,7 +153,7 @@ impl ToSqlite for Table { let mut values: Vec = Vec::new(); let mut parameters = Values::new(); - for cname in query.values.order.iter() { + for (cname, value) in query.values.values.iter() { let column = query.table.columns.get(cname.as_str()).unwrap(); // Get the column (might be an alias) @@ -162,8 +162,6 @@ impl ToSqlite for Table { column_name = column.alias.to_string(); } - let value = query.values.get(cname).unwrap(); - // Skip auto increment columns if column.column_type.is_auto_increment() { continue; @@ -210,7 +208,7 @@ impl ToSqlite for Table { let mut columns: Vec = Vec::new(); let mut parameters = Values::new(); - for cname in query.values.order.iter() { + for (cname, value) in query.values.values.iter() { let column = query.table.columns.get(cname.as_str()).unwrap(); // Skip if primary key @@ -223,8 +221,6 @@ impl ToSqlite for Table { column_name = column.alias.to_string(); } - let value = query.values.get(cname).unwrap(); - // Add to Values match value { crate::Value::Identifier(_) diff --git a/geekorm-core/src/builder/values/mod.rs b/geekorm-core/src/builder/values/mod.rs index 5f30f99..df08d6a 100644 --- a/geekorm-core/src/builder/values/mod.rs +++ b/geekorm-core/src/builder/values/mod.rs @@ -18,32 +18,27 @@ use crate::{ #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Values { /// List of values - pub(crate) values: Vec, - /// List of columns in the order they were added - pub(crate) order: Vec, + pub(crate) values: Vec<(String, Value)>, + // /// List of columns in the order they were added + // pub(crate) order: Vec, } impl Values { /// Create a new instance of Values pub fn new() -> Self { - Values { - values: Vec::new(), - order: Vec::new(), - } + Values { values: Vec::new() } } /// Push a value to the list of values pub fn push(&mut self, column: String, value: impl Into) { - self.order.push(column.clone()); - self.values.push(value.into()); + self.values.push((column, value.into())); } /// Get a value by index from the list of values pub fn get(&self, column: &String) -> Option<&Value> { - match self.order.iter().enumerate().find(|(_, o)| *o == column) { - Some((i, _)) => self.values.get(i), - None => None, - } + self.values + .iter() + .find_map(|(c, o)| if c == column { Some(o) } else { None }) } /// Length / Count of the values stored @@ -57,10 +52,9 @@ impl IntoIterator for Values { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - self.order + self.values .into_iter() - .enumerate() - .map(move |(index, _)| self.values[index].clone()) + .map(|(_, v)| v) .collect::>() .into_iter() } diff --git a/geekorm-core/src/error.rs b/geekorm-core/src/error.rs index b5d0108..aec7cd4 100644 --- a/geekorm-core/src/error.rs +++ b/geekorm-core/src/error.rs @@ -61,4 +61,10 @@ pub enum Error { #[cfg(feature = "rusqlite")] #[error("RuSQLite Error occurred: {0}")] RuSQLiteError(String), + + /// Query Syntax Error + #[error( + "Query Syntax Error: {0}\n -> {1}\nPlease report this error to the GeekORM developers" + )] + QuerySyntaxError(String, String), } diff --git a/geekorm-core/src/queries/builder.rs b/geekorm-core/src/queries/builder.rs index b891a18..650771a 100644 --- a/geekorm-core/src/queries/builder.rs +++ b/geekorm-core/src/queries/builder.rs @@ -86,7 +86,7 @@ pub struct QueryBuilder { pub(crate) joins: TableJoins, - /// The values to use (where / insert) + /// The values are used for data inserted into the database pub(crate) values: Values, pub(crate) error: Option, @@ -331,10 +331,23 @@ impl QueryBuilder { } /// Build a Query from the QueryBuilder and perform some checks - pub fn build(&self) -> Result { + pub fn build(&mut self) -> Result { if let Some(ref error) = self.error { return Err(error.clone()); } + + // Check the last where condition + let mut pop_where_condition = false; + if let Some(last) = self.where_clause.last() { + if last == &WhereCondition::Or.to_sqlite() || last == &WhereCondition::And.to_sqlite() { + pop_where_condition = true; + } + } + // Pop the last where condition + if pop_where_condition { + self.where_clause.pop(); + } + match self.query_type { QueryType::Create => { let query = self.table.on_create(self)?; From fdb31518f3226064a8266d36704fc39570e0fec0 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Sat, 11 Jan 2025 00:59:20 +0000 Subject: [PATCH 3/3] feat: Update Cargo --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 27d88ff..e6ad5d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,11 @@ name = "pagination" path = "./examples/pagination.rs" required-features = ["all", "libsql", "pagination"] +[[example]] +name = "filter" +path = "./examples/filter.rs" +required-features = ["all", "libsql"] + [[example]] name = "turso-libsql" path = "./examples/turso-libsql/src/main.rs"