Skip to content
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
6 changes: 6 additions & 0 deletions graph/src/data/query/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub enum QueryExecutionError {
FilterNotSupportedError(String, String),
UnknownField(Pos, String, String),
EmptyQuery,
InvalidOrFilterStructure(Vec<String>, String),
SubgraphDeploymentIdError(String),
RangeArgumentsError(&'static str, u32, i64),
InvalidFilterError,
Expand Down Expand Up @@ -97,6 +98,7 @@ impl QueryExecutionError {
| ChildFilterNestingNotSupportedError(_, _)
| UnknownField(_, _, _)
| EmptyQuery
| InvalidOrFilterStructure(_, _)
| SubgraphDeploymentIdError(_)
| InvalidFilterError
| EntityFieldError(_, _)
Expand Down Expand Up @@ -210,6 +212,10 @@ impl fmt::Display for QueryExecutionError {
write!(f, "The `{}` argument must be between 0 and {}, but is {}", arg, max, actual)
}
InvalidFilterError => write!(f, "Filter must by an object"),
InvalidOrFilterStructure(fields, example) => {
write!(f, "Cannot mix column filters with 'or' operator at the same level. Found column filter(s) {} alongside 'or' operator.\n\n{}",
fields.join(", "), example)
}
EntityFieldError(e, a) => {
write!(f, "Entity `{}` has no attribute `{}`", e, a)
}
Expand Down
261 changes: 261 additions & 0 deletions graphql/src/store/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,34 @@ fn build_filter_from_object<'a>(
object: &Object,
schema: &InputSchema,
) -> Result<Vec<EntityFilter>, QueryExecutionError> {
// Check if we have both column filters and 'or' operator at the same level
if let Some(_) = object.get("or") {
let column_filters: Vec<String> = object
.iter()
.filter_map(|(key, _)| {
if key != "or" && key != "and" && key != "_change_block" {
Some(format!("'{}'", key))
} else {
None
}
})
.collect();

if !column_filters.is_empty() {
let filter_list = column_filters.join(", ");
let example = format!(
"Instead of:\nwhere: {{ {}, or: [...] }}\n\nUse:\nwhere: {{ or: [{{ {}, ... }}, {{ {}, ... }}] }}",
filter_list,
filter_list,
filter_list
);
return Err(QueryExecutionError::InvalidOrFilterStructure(
column_filters,
example,
));
}
}

object
.iter()
.map(|(key, value)| {
Expand Down Expand Up @@ -957,4 +985,237 @@ mod tests {
Some(EntityFilter::And(vec![EntityFilter::ChangeBlockGte(10)]))
)
}

#[test]
fn build_query_detects_invalid_or_filter_structure() {
// Test that mixing column filters with 'or' operator produces a helpful error
let query_field = default_field_with(
"where",
r::Value::Object(Object::from_iter(vec![
("name".into(), r::Value::String("John".to_string())),
(
"or".into(),
r::Value::List(vec![r::Value::Object(Object::from_iter(vec![(
"email".into(),
r::Value::String("[email protected]".to_string()),
)]))]),
),
])),
);

// We only allow one entity type in these tests
assert_eq!(query_field.selection_set.fields().count(), 1);
let obj_type = query_field
.selection_set
.fields()
.map(|(obj, _)| &obj.name)
.next()
.expect("there is one object type");
let Some(object) = INPUT_SCHEMA.object_or_interface(obj_type, None) else {
panic!("object type {} not found", obj_type);
};

let result = build_query(
&object,
BLOCK_NUMBER_MAX,
&query_field,
std::u32::MAX,
std::u32::MAX,
&*INPUT_SCHEMA,
);

assert!(result.is_err());
let error = result.unwrap_err();

// Check that we get the specific error we expect
match error {
graph::data::query::QueryExecutionError::InvalidOrFilterStructure(fields, example) => {
assert_eq!(fields, vec!["'name'"]);
assert!(example.contains("Instead of:"));
assert!(example.contains("where: { 'name', or: [...] }"));
assert!(example.contains("Use:"));
assert!(example.contains("where: { or: [{ 'name', ... }, { 'name', ... }] }"));
}
_ => panic!("Expected InvalidOrFilterStructure error, got: {}", error),
}
}

#[test]
fn build_query_detects_invalid_or_filter_structure_multiple_fields() {
// Test that multiple column filters with 'or' operator are all reported
let query_field = default_field_with(
"where",
r::Value::Object(Object::from_iter(vec![
("name".into(), r::Value::String("John".to_string())),
(
"email".into(),
r::Value::String("[email protected]".to_string()),
),
(
"or".into(),
r::Value::List(vec![r::Value::Object(Object::from_iter(vec![(
"name".into(),
r::Value::String("Jane".to_string()),
)]))]),
),
])),
);

// We only allow one entity type in these tests
assert_eq!(query_field.selection_set.fields().count(), 1);
let obj_type = query_field
.selection_set
.fields()
.map(|(obj, _)| &obj.name)
.next()
.expect("there is one object type");
let Some(object) = INPUT_SCHEMA.object_or_interface(obj_type, None) else {
panic!("object type {} not found", obj_type);
};

let result = build_query(
&object,
BLOCK_NUMBER_MAX,
&query_field,
std::u32::MAX,
std::u32::MAX,
&*INPUT_SCHEMA,
);

assert!(result.is_err());
let error = result.unwrap_err();

// Check that we get the specific error we expect
match error {
graph::data::query::QueryExecutionError::InvalidOrFilterStructure(fields, example) => {
// Should detect both column filters
assert_eq!(fields.len(), 2);
assert!(fields.contains(&"'name'".to_string()));
assert!(fields.contains(&"'email'".to_string()));
assert!(example.contains("Instead of:"));
assert!(example.contains("Use:"));
}
_ => panic!("Expected InvalidOrFilterStructure error, got: {}", error),
}
}

#[test]
fn build_query_allows_valid_or_filter_structure() {
// Test that valid 'or' filters without column filters at the same level work correctly
let query_field = default_field_with(
"where",
r::Value::Object(Object::from_iter(vec![(
"or".into(),
r::Value::List(vec![
r::Value::Object(Object::from_iter(vec![(
"name".into(),
r::Value::String("John".to_string()),
)])),
r::Value::Object(Object::from_iter(vec![(
"email".into(),
r::Value::String("[email protected]".to_string()),
)])),
]),
)])),
);

// This should not produce an error
let result = query(&query_field);
assert!(result.filter.is_some());

// Verify that the filter is correctly structured
match result.filter.unwrap() {
EntityFilter::And(filters) => {
assert_eq!(filters.len(), 1);
match &filters[0] {
EntityFilter::Or(_) => {
// This is expected - OR filter should be wrapped in AND
}
_ => panic!("Expected OR filter, got: {:?}", filters[0]),
}
}
_ => panic!("Expected AND filter with OR inside"),
}
}

#[test]
fn build_query_detects_invalid_or_filter_structure_with_operators() {
// Test that column filters with operators (like name_gt) are also detected
let query_field = default_field_with(
"where",
r::Value::Object(Object::from_iter(vec![
("name_gt".into(), r::Value::String("A".to_string())),
(
"or".into(),
r::Value::List(vec![r::Value::Object(Object::from_iter(vec![(
"email".into(),
r::Value::String("[email protected]".to_string()),
)]))]),
),
])),
);

// We only allow one entity type in these tests
assert_eq!(query_field.selection_set.fields().count(), 1);
let obj_type = query_field
.selection_set
.fields()
.map(|(obj, _)| &obj.name)
.next()
.expect("there is one object type");
let Some(object) = INPUT_SCHEMA.object_or_interface(obj_type, None) else {
panic!("object type {} not found", obj_type);
};

let result = build_query(
&object,
BLOCK_NUMBER_MAX,
&query_field,
std::u32::MAX,
std::u32::MAX,
&*INPUT_SCHEMA,
);

assert!(result.is_err());
let error = result.unwrap_err();

// Check that we get the specific error we expect
match error {
graph::data::query::QueryExecutionError::InvalidOrFilterStructure(fields, example) => {
assert_eq!(fields, vec!["'name_gt'"]);
assert!(example.contains("Instead of:"));
assert!(example.contains("where: { 'name_gt', or: [...] }"));
assert!(example.contains("Use:"));
assert!(example.contains("where: { or: [{ 'name_gt', ... }, { 'name_gt', ... }] }"));
}
_ => panic!("Expected InvalidOrFilterStructure error, got: {}", error),
}
}

#[test]
fn test_error_message_formatting() {
// Test that the error message is properly formatted
let fields = vec!["'age_gt'".to_string(), "'name'".to_string()];
let example = format!(
"Instead of:\nwhere: {{ {}, or: [...] }}\n\nUse:\nwhere: {{ or: [{{ {}, ... }}, {{ {}, ... }}] }}",
fields.join(", "),
fields.join(", "),
fields.join(", ")
);

let error =
graph::data::query::QueryExecutionError::InvalidOrFilterStructure(fields, example);
let error_msg = format!("{}", error);

println!("Error message:\n{}", error_msg);

// Verify the error message contains the key elements
assert!(error_msg.contains("Cannot mix column filters with 'or' operator"));
assert!(error_msg.contains("'age_gt', 'name'"));
assert!(error_msg.contains("Instead of:"));
assert!(error_msg.contains("Use:"));
assert!(error_msg.contains("where: { 'age_gt', 'name', or: [...] }"));
assert!(error_msg
.contains("where: { or: [{ 'age_gt', 'name', ... }, { 'age_gt', 'name', ... }] }"));
}
}