Skip to content

Commit

Permalink
graphql: AND/OR filter (#4080)
Browse files Browse the repository at this point in the history
* graphql/store: AND/OR filter

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>

* store: refactor build filter logic into fn

* graphql: env variable to disable filters

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
  • Loading branch information
saihaj and kamilkisiela authored Nov 10, 2022
1 parent f30efb8 commit 95c316a
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 82 deletions.
1 change: 1 addition & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ those.
- `SILENT_GRAPHQL_VALIDATIONS`: If `ENABLE_GRAPHQL_VALIDATIONS` is enabled, you are also able to just
silently print the GraphQL validation errors, without failing the actual query. Note: queries
might still fail as part of the later stage validations running, during GraphQL engine execution.
- `GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS`: disables the ability to use AND/OR filters. This is useful if we want to disable filters because of performance reasons.

### GraphQL caching

Expand Down
6 changes: 6 additions & 0 deletions graph/src/env/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ pub struct EnvVarsGraphQl {
/// Set by the flag `GRAPH_GRAPHQL_MAX_OPERATIONS_PER_CONNECTION`.
/// Defaults to 1000.
pub max_operations_per_connection: usize,
/// Set by the flag `GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS`. Off by default.
/// Disables AND/OR filters
pub disable_bool_filters: bool,
}

// This does not print any values avoid accidentally leaking any sensitive env vars
Expand Down Expand Up @@ -128,6 +131,7 @@ impl From<InnerGraphQl> for EnvVarsGraphQl {
warn_result_size: x.warn_result_size.0 .0,
error_result_size: x.error_result_size.0 .0,
max_operations_per_connection: x.max_operations_per_connection,
disable_bool_filters: x.disable_bool_filters.0,
}
}
}
Expand Down Expand Up @@ -173,4 +177,6 @@ pub struct InnerGraphQl {
error_result_size: WithDefaultUsize<NoUnderscores<usize>, { usize::MAX }>,
#[envconfig(from = "GRAPH_GRAPHQL_MAX_OPERATIONS_PER_CONNECTION", default = "1000")]
max_operations_per_connection: usize,
#[envconfig(from = "GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS", default = "false")]
pub disable_bool_filters: EnvVarBoolean,
}
36 changes: 33 additions & 3 deletions graphql/src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,30 @@ fn add_filter_type(
let mut generated_filter_fields = field_input_values(schema, fields)?;
generated_filter_fields.push(block_changed_filter_argument());

if !ENV_VARS.graphql.disable_bool_filters {
generated_filter_fields.push(InputValue {
position: Pos::default(),
description: None,
name: "and".to_string(),
value_type: Type::ListType(Box::new(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::ListType(Box::new(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 +993,9 @@ mod tests {
"favoritePet_",
"leastFavoritePet_",
"mostFavoritePets_",
"_change_block"
"_change_block",
"and",
"or"
]
.iter()
.map(ToString::to_string)
Expand Down Expand Up @@ -1046,7 +1072,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 +1198,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
12 changes: 10 additions & 2 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,17 @@ 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
231 changes: 154 additions & 77 deletions graphql/src/store/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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 +118,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 +161,164 @@ 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!(),
};
}

/// Iterate over the list and generate an EntityFilter from it
fn build_list_filter_from_value(
entity: ObjectOrInterface,
schema: &ApiSchema,
value: &r::Value,
) -> Result<Vec<EntityFilter>, QueryExecutionError> {
return match value {
r::Value::List(list) => Ok(list
.iter()
.map(|item| {
return match item {
r::Value::Object(object) => {
Ok(build_filter_from_object(entity, object, schema)?)
}
_ => Err(QueryExecutionError::InvalidFilterError),
};
})
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>()?
// Flatten all different EntityFilters into one list
.into_iter()
.flatten()
.collect::<Vec<EntityFilter>>()),
_ => Err(QueryExecutionError::InvalidFilterError),
};
}

/// build a filter which has list of nested filters
fn build_list_filter_from_object(
entity: ObjectOrInterface,
object: &Object,
schema: &ApiSchema,
) -> Result<Vec<EntityFilter>, QueryExecutionError> {
Ok(object
.iter()
.map(|(_, value)| {
return build_list_filter_from_value(entity, schema, value);
})
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>()?
.into_iter()
// We iterate an object so all entity filters are flattened into one list
.flatten()
.collect::<Vec<EntityFilter>>())
}

/// 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),
};
}

use self::sast::FilterOp::*;
let (field_name, op) = sast::parse_field_as_filter(key);
) -> 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);

let field = sast::get_field(entity, &field_name).ok_or_else(|| {
QueryExecutionError::EntityFieldError(
entity.name().to_owned(),
field_name.clone(),
)
})?;
Ok(match op {
And => {
if ENV_VARS.graphql.disable_bool_filters {
return Err(QueryExecutionError::NotSupported(
"Boolean filters are not supported".to_string(),
));
}

let ty = &field.field_type;
return Ok(EntityFilter::And(build_list_filter_from_object(
entity, object, schema,
)?));
}
Or => {
if ENV_VARS.graphql.disable_bool_filters {
return Err(QueryExecutionError::NotSupported(
"Boolean filters are not supported".to_string(),
));
}

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(),
))
}
},
return Ok(EntityFilter::Or(build_list_filter_from_object(
entity, object, schema,
)?));
}
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 +334,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

0 comments on commit 95c316a

Please sign in to comment.