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

feat: AND/OR query filters #3934

Closed
wants to merge 10 commits into from
29 changes: 25 additions & 4 deletions graphql/src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,22 @@ fn add_filter_type(
None => {
let mut generated_filter_fields = field_input_values(schema, fields)?;
generated_filter_fields.push(block_changed_filter_argument());

generated_filter_fields.push(InputValue {
position: Pos::default(),
description: None,
name: "and".to_string(),
value_type: Type::NamedType(filter_type_name.to_owned()),
default_value: None,
directives: vec![],
});
generated_filter_fields.push(InputValue {
position: Pos::default(),
description: None,
name: "or".to_string(),
value_type: Type::NamedType(filter_type_name.to_owned()),
default_value: None,
directives: vec![],
});
let typedef = TypeDefinition::InputObject(InputObjectType {
position: Pos::default(),
description: None,
Expand Down Expand Up @@ -969,7 +984,9 @@ mod tests {
"favoritePet_",
"leastFavoritePet_",
"mostFavoritePets_",
"_change_block"
"_change_block",
"and",
"or"
]
.iter()
.map(ToString::to_string)
Expand Down Expand Up @@ -1046,7 +1063,9 @@ mod tests {
"mostLovedBy_not_contains",
"mostLovedBy_not_contains_nocase",
"mostLovedBy_",
"_change_block"
"_change_block",
"and",
"or"
]
.iter()
.map(ToString::to_string)
Expand Down Expand Up @@ -1170,7 +1189,9 @@ mod tests {
"favoritePet_not_ends_with",
"favoritePet_not_ends_with_nocase",
"favoritePet_",
"_change_block"
"_change_block",
"and",
"or"
]
.iter()
.map(ToString::to_string)
Expand Down
13 changes: 10 additions & 3 deletions graphql/src/schema/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub(crate) enum FilterOp {
NotEndsWithNoCase,
Equal,
Child,
And,
Or,
}

/// Split a "name_eq" style name into an attribute ("name") and a filter op (`Equal`).
Expand Down Expand Up @@ -67,11 +69,16 @@ pub(crate) fn parse_field_as_filter(key: &str) -> (String, FilterOp) {
k if k.ends_with("_ends_with") => ("_ends_with", FilterOp::EndsWith),
k if k.ends_with("_ends_with_nocase") => ("_ends_with_nocase", FilterOp::EndsWithNoCase),
k if k.ends_with("_") => ("_", FilterOp::Child),
k if k.eq("and") => ("and", FilterOp::And),
k if k.eq("or") => ("or", FilterOp::Or),
_ => ("", FilterOp::Equal),
};

// Strip the operator suffix to get the attribute.
(key.trim_end_matches(suffix).to_owned(), op)
return match op {
FilterOp::And => (key.to_owned(), op),
FilterOp::Or => (key.to_owned(), op),
// Strip the operator suffix to get the attribute.
_ => (key.trim_end_matches(suffix).to_owned(), op),
};
}

/// An `ObjectType` with `Hash` and `Eq` derived from the name.
Expand Down
216 changes: 139 additions & 77 deletions graphql/src/store/query.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
use std::mem::discriminant;

use anyhow::Result;
use graph::data::graphql::ext::DirectiveFinder;
use graph::data::graphql::TypeExt as _;
use graph::data::value::Object;
Expand All @@ -9,7 +10,7 @@ use graph::prelude::*;
use graph::{components::store::EntityType, data::graphql::ObjectOrInterface};

use crate::execution::ast as a;
use crate::schema::ast as sast;
use crate::schema::ast::{self as sast, FilterOp};

use super::prefetch::SelectedAttributes;

Expand Down Expand Up @@ -118,7 +119,7 @@ fn build_filter(
) -> Result<Option<EntityFilter>, QueryExecutionError> {
match field.argument_value("where") {
Some(r::Value::Object(object)) => match build_filter_from_object(entity, object, schema) {
Ok(filter) => Ok(Some(filter)),
Ok(filter) => Ok(Some(EntityFilter::And(filter))),
Err(e) => Err(e),
},
Some(r::Value::Null) => Ok(None),
Expand Down Expand Up @@ -161,91 +162,148 @@ fn parse_change_block_filter(value: &r::Value) -> Result<BlockNumber, QueryExecu
}
}

/// Parses a GraphQL Filter Value into an EntityFilter.
fn build_entity_filter(
field_name: String,
operation: FilterOp,
store_value: Value,
) -> Result<EntityFilter, QueryExecutionError> {
return match operation {
FilterOp::Not => Ok(EntityFilter::Not(field_name, store_value)),
FilterOp::GreaterThan => Ok(EntityFilter::GreaterThan(field_name, store_value)),
FilterOp::LessThan => Ok(EntityFilter::LessThan(field_name, store_value)),
FilterOp::GreaterOrEqual => Ok(EntityFilter::GreaterOrEqual(field_name, store_value)),
FilterOp::LessOrEqual => Ok(EntityFilter::LessOrEqual(field_name, store_value)),
FilterOp::In => Ok(EntityFilter::In(
field_name,
list_values(store_value, "_in")?,
)),
FilterOp::NotIn => Ok(EntityFilter::NotIn(
field_name,
list_values(store_value, "_not_in")?,
)),
FilterOp::Contains => Ok(EntityFilter::Contains(field_name, store_value)),
FilterOp::ContainsNoCase => Ok(EntityFilter::ContainsNoCase(field_name, store_value)),
FilterOp::NotContains => Ok(EntityFilter::NotContains(field_name, store_value)),
FilterOp::NotContainsNoCase => Ok(EntityFilter::NotContainsNoCase(field_name, store_value)),
FilterOp::StartsWith => Ok(EntityFilter::StartsWith(field_name, store_value)),
FilterOp::StartsWithNoCase => Ok(EntityFilter::StartsWithNoCase(field_name, store_value)),
FilterOp::NotStartsWith => Ok(EntityFilter::NotStartsWith(field_name, store_value)),
FilterOp::NotStartsWithNoCase => {
Ok(EntityFilter::NotStartsWithNoCase(field_name, store_value))
}
FilterOp::EndsWith => Ok(EntityFilter::EndsWith(field_name, store_value)),
FilterOp::EndsWithNoCase => Ok(EntityFilter::EndsWithNoCase(field_name, store_value)),
FilterOp::NotEndsWith => Ok(EntityFilter::NotEndsWith(field_name, store_value)),
FilterOp::NotEndsWithNoCase => Ok(EntityFilter::NotEndsWithNoCase(field_name, store_value)),
FilterOp::Equal => Ok(EntityFilter::Equal(field_name, store_value)),
_ => unreachable!(),
};
}

/// Parses a GraphQL input object into an EntityFilter, if present.
fn build_filter_from_object(
entity: ObjectOrInterface,
object: &Object,
schema: &ApiSchema,
) -> Result<EntityFilter, QueryExecutionError> {
Ok(EntityFilter::And({
object
.iter()
.map(|(key, value)| {
// Special handling for _change_block input filter since its not a
// standard entity filter that is based on entity structure/fields
if key == "_change_block" {
return match parse_change_block_filter(value) {
Ok(block_number) => Ok(EntityFilter::ChangeBlockGte(block_number)),
Err(e) => Err(e),
) -> Result<Vec<EntityFilter>, QueryExecutionError> {
Ok(object
.iter()
.map(|(key, value)| {
// Special handling for _change_block input filter since its not a
// standard entity filter that is based on entity structure/fields
if key == "_change_block" {
return match parse_change_block_filter(value) {
Ok(block_number) => Ok(EntityFilter::ChangeBlockGte(block_number)),
Err(e) => Err(e),
};
}
use self::sast::FilterOp::*;
let (field_name, op) = sast::parse_field_as_filter(key);
Ok(match op {
And => {
return match value {
r::Value::Object(object) => {
return Ok(EntityFilter::And(build_filter_from_object(
entity, object, schema,
)?));
}
_ => Err(QueryExecutionError::InvalidFilterError),
};
}

use self::sast::FilterOp::*;
let (field_name, op) = sast::parse_field_as_filter(key);

let field = sast::get_field(entity, &field_name).ok_or_else(|| {
QueryExecutionError::EntityFieldError(
entity.name().to_owned(),
field_name.clone(),
)
})?;

let ty = &field.field_type;

Ok(match op {
Child => match value {
DataValue::Object(obj) => {
build_child_filter_from_object(entity, field_name, obj, schema)?
}
_ => {
return Err(QueryExecutionError::AttributeTypeError(
value.to_string(),
ty.to_string(),
))
Or => {
return match value {
r::Value::Object(object) => {
let filters = object
.iter()
.map(|(key, value)| {
let (field_name, op) = sast::parse_field_as_filter(key);
let field =
sast::get_field(entity, &field_name).ok_or_else(|| {
// When we have `AND`/`OR` filters we can not get a field back
// Instead we want to building the entity filter from the object
// If it works out great otherwise we return an error
return build_filter_from_object(
entity, object, schema,
);
});
return match field {
Ok(field) => {
let ty = &field.field_type;
let store_value = Value::from_query_value(value, ty)?;
return build_entity_filter(
field_name,
op,
store_value,
);
}
Err(result) => match result {
Ok(filter) => Ok(EntityFilter::Or(filter)),
Err(e) => Err(e),
},
};
})
.collect::<Result<Vec<EntityFilter>, QueryExecutionError>>();
return match filters {
Ok(f) => Ok(EntityFilter::Or(f)),
Err(e) => Err(e),
};
}
},
_ => Err(QueryExecutionError::InvalidFilterError),
};
}
Child => match value {
DataValue::Object(obj) => {
build_child_filter_from_object(entity, field_name, obj, schema)?
}
_ => {
let store_value = Value::from_query_value(value, ty)?;

match op {
Not => EntityFilter::Not(field_name, store_value),
GreaterThan => EntityFilter::GreaterThan(field_name, store_value),
LessThan => EntityFilter::LessThan(field_name, store_value),
GreaterOrEqual => EntityFilter::GreaterOrEqual(field_name, store_value),
LessOrEqual => EntityFilter::LessOrEqual(field_name, store_value),
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),
_ => unreachable!(),
}
let field = sast::get_field(entity, &field_name).ok_or_else(|| {
QueryExecutionError::EntityFieldError(
entity.name().to_owned(),
field_name.clone(),
)
})?;
let ty = &field.field_type;
return Err(QueryExecutionError::AttributeTypeError(
value.to_string(),
ty.to_string(),
));
}
})
},
_ => {
let field = sast::get_field(entity, &field_name).ok_or_else(|| {
QueryExecutionError::EntityFieldError(
entity.name().to_owned(),
field_name.clone(),
)
})?;
let ty = &field.field_type;
let store_value = Value::from_query_value(value, ty)?;
return build_entity_filter(field_name, op, store_value);
}
})
.collect::<Result<Vec<EntityFilter>, QueryExecutionError>>()?
}))
})
.collect::<Result<Vec<EntityFilter>, QueryExecutionError>>()?)
}

fn build_child_filter_from_object(
Expand All @@ -261,7 +319,11 @@ fn build_child_filter_from_object(
let child_entity = schema
.object_or_interface(type_name)
.ok_or(QueryExecutionError::InvalidFilterError)?;
let filter = Box::new(build_filter_from_object(child_entity, object, schema)?);
let filter = Box::new(EntityFilter::And(build_filter_from_object(
child_entity,
object,
schema,
)?));
let derived = field.is_derived();
let attr = match derived {
true => sast::get_derived_from_field(child_entity, field)
Expand Down
Loading