diff --git a/graph/src/components/store/mod.rs b/graph/src/components/store/mod.rs index 69c459bb0ec..54d7d50b567 100644 --- a/graph/src/components/store/mod.rs +++ b/graph/src/components/store/mod.rs @@ -163,11 +163,17 @@ pub enum EntityFilter { In(Attribute, Vec), NotIn(Attribute, Vec), Contains(Attribute, Value), + ContainsNoCase(Attribute, Value), NotContains(Attribute, Value), + NotContainsNoCase(Attribute, Value), StartsWith(Attribute, Value), + StartsWithNoCase(Attribute, Value), NotStartsWith(Attribute, Value), + NotStartsWithNoCase(Attribute, Value), EndsWith(Attribute, Value), + EndsWithNoCase(Attribute, Value), NotEndsWith(Attribute, Value), + NotEndsWithNoCase(Attribute, Value), } // Define some convenience methods diff --git a/graphql/src/schema/api.rs b/graphql/src/schema/api.rs index 0691fd5ea24..68ec9310cc3 100644 --- a/graphql/src/schema/api.rs +++ b/graphql/src/schema/api.rs @@ -275,11 +275,17 @@ fn field_scalar_filter_input_values( "in", "not_in", "contains", + "contains_nocase", "not_contains", + "not_contains_nocase", "starts_with", + "starts_with_nocase", "not_starts_with", + "not_starts_with_nocase", "ends_with", + "ends_with_nocase", "not_ends_with", + "not_ends_with_nocase", ], _ => vec!["", "not"], } @@ -341,18 +347,25 @@ fn field_list_filter_input_values( }; Some( - vec!["", "not", "contains", "not_contains"] - .into_iter() - .map(|filter_type| { - input_value( - &field.name, - filter_type, - Type::ListType(Box::new(Type::NonNullType(Box::new( - input_field_type.clone(), - )))), - ) - }) - .collect(), + vec![ + "", + "not", + "contains", + "contains_nocase", + "not_contains", + "not_contains_nocase", + ] + .into_iter() + .map(|filter_type| { + input_value( + &field.name, + filter_type, + Type::ListType(Box::new(Type::NonNullType(Box::new( + input_field_type.clone(), + )))), + ) + }) + .collect(), ) }) } @@ -864,19 +877,29 @@ mod tests { "name_in", "name_not_in", "name_contains", + "name_contains_nocase", "name_not_contains", + "name_not_contains_nocase", "name_starts_with", + "name_starts_with_nocase", "name_not_starts_with", + "name_not_starts_with_nocase", "name_ends_with", + "name_ends_with_nocase", "name_not_ends_with", + "name_not_ends_with_nocase", "favoritePetNames", "favoritePetNames_not", "favoritePetNames_contains", + "favoritePetNames_contains_nocase", "favoritePetNames_not_contains", + "favoritePetNames_not_contains_nocase", "pets", "pets_not", "pets_contains", + "pets_contains_nocase", "pets_not_contains", + "pets_not_contains_nocase", "favoriteFurType", "favoriteFurType_not", "favoriteFurType_in", @@ -890,11 +913,17 @@ mod tests { "favoritePet_in", "favoritePet_not_in", "favoritePet_contains", + "favoritePet_contains_nocase", "favoritePet_not_contains", + "favoritePet_not_contains_nocase", "favoritePet_starts_with", + "favoritePet_starts_with_nocase", "favoritePet_not_starts_with", + "favoritePet_not_starts_with_nocase", "favoritePet_ends_with", + "favoritePet_ends_with_nocase", "favoritePet_not_ends_with", + "favoritePet_not_ends_with_nocase", ] .iter() .map(ToString::to_string) diff --git a/graphql/src/schema/ast.rs b/graphql/src/schema/ast.rs index 015aac07f82..29c833ac419 100644 --- a/graphql/src/schema/ast.rs +++ b/graphql/src/schema/ast.rs @@ -21,11 +21,17 @@ pub(crate) enum FilterOp { In, NotIn, Contains, + ContainsNoCase, NotContains, + NotContainsNoCase, StartsWith, + StartsWithNoCase, NotStartsWith, + NotStartsWithNoCase, EndsWith, + EndsWithNoCase, NotEndsWith, + NotEndsWithNoCase, Equal, } @@ -40,11 +46,25 @@ pub(crate) fn parse_field_as_filter(key: &str) -> (String, FilterOp) { k if k.ends_with("_not_in") => ("_not_in", FilterOp::NotIn), k if k.ends_with("_in") => ("_in", FilterOp::In), k if k.ends_with("_not_contains") => ("_not_contains", FilterOp::NotContains), + k if k.ends_with("_not_contains_nocase") => { + ("_not_contains_nocase", FilterOp::NotContainsNoCase) + } k if k.ends_with("_contains") => ("_contains", FilterOp::Contains), + k if k.ends_with("_contains_nocase") => ("_contains_nocase", FilterOp::ContainsNoCase), k if k.ends_with("_not_starts_with") => ("_not_starts_with", FilterOp::NotStartsWith), + k if k.ends_with("_not_starts_with_nocase") => { + ("_not_starts_with_nocase", FilterOp::NotStartsWithNoCase) + } k if k.ends_with("_not_ends_with") => ("_not_ends_with", FilterOp::NotEndsWith), + k if k.ends_with("_not_ends_with_nocase") => { + ("_not_ends_with_nocase", FilterOp::NotEndsWithNoCase) + } k if k.ends_with("_starts_with") => ("_starts_with", FilterOp::StartsWith), + k if k.ends_with("_starts_with_nocase") => { + ("_starts_with_nocase", FilterOp::StartsWithNoCase) + } k if k.ends_with("_ends_with") => ("_ends_with", FilterOp::EndsWith), + k if k.ends_with("_ends_with_nocase") => ("_ends_with_nocase", FilterOp::EndsWithNoCase), _ => ("", FilterOp::Equal), }; diff --git a/graphql/src/store/query.rs b/graphql/src/store/query.rs index 88d9f69250d..01b093654e5 100644 --- a/graphql/src/store/query.rs +++ b/graphql/src/store/query.rs @@ -173,11 +173,19 @@ fn build_filter_from_object( In => EntityFilter::In(field_name, list_values(store_value, "_in")?), NotIn => EntityFilter::NotIn(field_name, list_values(store_value, "_not_in")?), Contains => EntityFilter::Contains(field_name, store_value), + ContainsNoCase => EntityFilter::ContainsNoCase(field_name, store_value), NotContains => EntityFilter::NotContains(field_name, store_value), + NotContainsNoCase => EntityFilter::NotContainsNoCase(field_name, store_value), StartsWith => EntityFilter::StartsWith(field_name, store_value), + StartsWithNoCase => EntityFilter::StartsWithNoCase(field_name, store_value), NotStartsWith => EntityFilter::NotStartsWith(field_name, store_value), + NotStartsWithNoCase => { + EntityFilter::NotStartsWithNoCase(field_name, store_value) + } EndsWith => EntityFilter::EndsWith(field_name, store_value), + EndsWithNoCase => EntityFilter::EndsWithNoCase(field_name, store_value), NotEndsWith => EntityFilter::NotEndsWith(field_name, store_value), + NotEndsWithNoCase => EntityFilter::NotEndsWithNoCase(field_name, store_value), Equal => EntityFilter::Equal(field_name, store_value), }) }) diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index 6168e268f7c..d7eeaca64d5 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -830,7 +830,9 @@ impl<'a> QueryFilter<'a> { } Contains(attr, _) + | ContainsNoCase(attr, _) | NotContains(attr, _) + | NotContainsNoCase(attr, _) | Equal(attr, _) | Not(attr, _) | GreaterThan(attr, _) @@ -840,9 +842,13 @@ impl<'a> QueryFilter<'a> { | In(attr, _) | NotIn(attr, _) | StartsWith(attr, _) + | StartsWithNoCase(attr, _) | NotStartsWith(attr, _) + | NotStartsWithNoCase(attr, _) | EndsWith(attr, _) - | NotEndsWith(attr, _) => { + | EndsWithNoCase(attr, _) + | NotEndsWith(attr, _) + | NotEndsWithNoCase(attr, _) => { table.column_for_field(attr)?; } } @@ -889,18 +895,20 @@ impl<'a> QueryFilter<'a> { attribute: &Attribute, value: &Value, negated: bool, + strict: bool, mut out: AstPass, ) -> QueryResult<()> { let column = self.column(attribute); - + let operation = match (strict, negated) { + (true, true) => " not like ", + (true, false) => " like ", + (false, true) => " not ilike ", + (false, false) => " ilike ", + }; match value { Value::String(s) => { out.push_identifier(column.name.as_str())?; - if negated { - out.push_sql(" not like "); - } else { - out.push_sql(" like ") - }; + out.push_sql(operation); if s.starts_with('%') || s.ends_with('%') { out.push_bind_param::(s)?; } else { @@ -1158,8 +1166,10 @@ impl<'a> QueryFragment for QueryFilter<'a> { And(filters) => self.binary_op(filters, " and ", " true ", out)?, Or(filters) => self.binary_op(filters, " or ", " false ", out)?, - Contains(attr, value) => self.contains(attr, value, false, out)?, - NotContains(attr, value) => self.contains(attr, value, true, out)?, + Contains(attr, value) => self.contains(attr, value, false, true, out)?, + ContainsNoCase(attr, value) => self.contains(attr, value, false, false, out)?, + NotContains(attr, value) => self.contains(attr, value, true, true, out)?, + NotContainsNoCase(attr, value) => self.contains(attr, value, true, false, out)?, Equal(attr, value) => self.equals(attr, value, c::Equal, out)?, Not(attr, value) => self.equals(attr, value, c::NotEqual, out)?, @@ -1175,13 +1185,25 @@ impl<'a> QueryFragment for QueryFilter<'a> { StartsWith(attr, value) => { self.starts_or_ends_with(attr, value, " like ", true, out)? } + StartsWithNoCase(attr, value) => { + self.starts_or_ends_with(attr, value, " ilike ", true, out)? + } NotStartsWith(attr, value) => { self.starts_or_ends_with(attr, value, " not like ", true, out)? } + NotStartsWithNoCase(attr, value) => { + self.starts_or_ends_with(attr, value, " not ilike ", true, out)? + } EndsWith(attr, value) => self.starts_or_ends_with(attr, value, " like ", false, out)?, + EndsWithNoCase(attr, value) => { + self.starts_or_ends_with(attr, value, " ilike ", false, out)? + } NotEndsWith(attr, value) => { self.starts_or_ends_with(attr, value, " not like ", false, out)? } + NotEndsWithNoCase(attr, value) => { + self.starts_or_ends_with(attr, value, " not ilike ", false, out)? + } } Ok(()) }