Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter Feature and Value updates #169

Merged
merged 3 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
62 changes: 62 additions & 0 deletions examples/filter.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
#[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<libsql::Connection> {
// 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)
}
36 changes: 19 additions & 17 deletions geekorm-core/src/backends/libsql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand All @@ -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<libsql::Value> = match convert_values(&query) {
Ok(parameters) => parameters,
Expand Down Expand Up @@ -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(),
));
}
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(())
}
Expand All @@ -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(),
));
}
};

Expand Down Expand Up @@ -487,19 +496,12 @@ fn convert_values(query: &crate::Query) -> Result<Vec<libsql::Value>, crate::Err
let mut parameters: Vec<libsql::Value> = 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()) {
Expand Down
34 changes: 34 additions & 0 deletions geekorm-core/src/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value>)>,
) -> Result<Vec<Self>, 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",
Expand Down
8 changes: 2 additions & 6 deletions geekorm-core/src/builder/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl ToSqlite for Table {
let mut values: Vec<String> = 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)
Expand All @@ -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;
Expand Down Expand Up @@ -210,7 +208,7 @@ impl ToSqlite for Table {
let mut columns: Vec<String> = 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
Expand All @@ -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(_)
Expand Down
26 changes: 10 additions & 16 deletions geekorm-core/src/builder/values/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,27 @@ use crate::{
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Values {
/// List of values
pub(crate) values: Vec<Value>,
/// List of columns in the order they were added
pub(crate) order: Vec<String>,
pub(crate) values: Vec<(String, Value)>,
// /// List of columns in the order they were added
// pub(crate) order: Vec<String>,
}

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<Value>) {
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
Expand All @@ -57,10 +52,9 @@ impl IntoIterator for Values {
type IntoIter = std::vec::IntoIter<Value>;

fn into_iter(self) -> Self::IntoIter {
self.order
self.values
.into_iter()
.enumerate()
.map(move |(index, _)| self.values[index].clone())
.map(|(_, v)| v)
.collect::<Vec<Value>>()
.into_iter()
}
Expand Down
6 changes: 6 additions & 0 deletions geekorm-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
17 changes: 15 additions & 2 deletions geekorm-core/src/queries/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Error>,
Expand Down Expand Up @@ -331,10 +331,23 @@ impl QueryBuilder {
}

/// Build a Query from the QueryBuilder and perform some checks
pub fn build(&self) -> Result<Query, crate::Error> {
pub fn build(&mut self) -> Result<Query, crate::Error> {
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)?;
Expand Down
Loading