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

graph,graphql,store: nested sorting #3096

Merged
merged 1 commit into from
Feb 14, 2023
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
3 changes: 3 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ those.
- `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.
- `GRAPH_GRAPHQL_DISABLE_CHILD_SORTING`: disables the ability to use child-based
sorting. This is useful if we want to disable child-based sorting because of
performance reasons.
- `GRAPH_GRAPHQL_TRACE_TOKEN`: the token to use to enable query tracing for
a GraphQL request. If this is set, requests that have a header
`X-GraphTraceQuery` set to this value will include a trace of the SQL
Expand Down
22 changes: 22 additions & 0 deletions graph/src/components/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,35 @@ impl EntityFilter {
}
}

/// Holds the information needed to query a store.
#[derive(Clone, Debug, PartialEq)]
pub struct EntityOrderByChildInfo {
/// The attribute of the child entity that is used to order the results.
pub sort_by_attribute: Attribute,
/// The attribute that is used to join to the parent and child entity.
pub join_attribute: Attribute,
/// If true, the child entity is derived from the parent entity.
pub derived: bool,
dotansimha marked this conversation as resolved.
Show resolved Hide resolved
}

/// Holds the information needed to order the results of a query based on nested entities.
#[derive(Clone, Debug, PartialEq)]
pub enum EntityOrderByChild {
Object(EntityOrderByChildInfo, EntityType),
Interface(EntityOrderByChildInfo, Vec<EntityType>),
}

/// The order in which entities should be restored from a store.
#[derive(Clone, Debug, PartialEq)]
pub enum EntityOrder {
/// Order ascending by the given attribute. Use `id` as a tie-breaker
Ascending(String, ValueType),
/// Order descending by the given attribute. Use `id` as a tie-breaker
Descending(String, ValueType),
/// Order ascending by the given attribute of a child entity. Use `id` as a tie-breaker
ChildAscending(EntityOrderByChild),
/// Order descending by the given attribute of a child entity. Use `id` as a tie-breaker
ChildDescending(EntityOrderByChild),
/// Order by the `id` of the entities
Default,
/// Do not order at all. This speeds up queries where we know that
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 @@ -87,6 +87,9 @@ pub struct EnvVarsGraphQl {
/// Set by the flag `GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS`. Off by default.
/// Disables AND/OR filters
pub disable_bool_filters: bool,
/// Set by the flag `GRAPH_GRAPHQL_DISABLE_CHILD_SORTING`. Off by default.
/// Disables child-based sorting
pub disable_child_sorting: bool,
/// Set by `GRAPH_GRAPHQL_TRACE_TOKEN`, the token to use to enable query
/// tracing for a GraphQL request. If this is set, requests that have a
/// header `X-GraphTraceQuery` set to this value will include a trace of
Expand Down Expand Up @@ -137,6 +140,7 @@ impl From<InnerGraphQl> for EnvVarsGraphQl {
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,
disable_child_sorting: x.disable_child_sorting.0,
query_trace_token: x.query_trace_token,
}
}
Expand Down Expand Up @@ -185,6 +189,8 @@ pub struct InnerGraphQl {
max_operations_per_connection: usize,
#[envconfig(from = "GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS", default = "false")]
pub disable_bool_filters: EnvVarBoolean,
#[envconfig(from = "GRAPH_GRAPHQL_DISABLE_CHILD_SORTING", default = "false")]
pub disable_child_sorting: EnvVarBoolean,
#[envconfig(from = "GRAPH_GRAPHQL_TRACE_TOKEN", default = "")]
query_trace_token: String,
}
9 changes: 5 additions & 4 deletions graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ pub mod prelude {
pub use crate::components::store::{
AttributeNames, BlockNumber, CachedEthereumCall, ChainStore, Child, ChildMultiplicity,
EntityCache, EntityChange, EntityChangeOperation, EntityCollection, EntityFilter,
EntityLink, EntityModification, EntityOperation, EntityOrder, EntityQuery, EntityRange,
EntityWindow, EthereumCallCache, ParentLink, PartialBlockPtr, PoolWaitStats, QueryStore,
QueryStoreManager, StoreError, StoreEvent, StoreEventStream, StoreEventStreamBox,
SubgraphStore, UnfailOutcome, WindowAttribute, BLOCK_NUMBER_MAX,
EntityLink, EntityModification, EntityOperation, EntityOrder, EntityOrderByChild,
EntityOrderByChildInfo, EntityQuery, EntityRange, EntityWindow, EthereumCallCache,
ParentLink, PartialBlockPtr, PoolWaitStats, QueryStore, QueryStoreManager, StoreError,
StoreEvent, StoreEventStream, StoreEventStreamBox, SubgraphStore, UnfailOutcome,
WindowAttribute, BLOCK_NUMBER_MAX,
};
pub use crate::components::subgraph::{
BlockState, DataSourceTemplateInfo, HostMetrics, RuntimeHost, RuntimeHostBuilder,
Expand Down
261 changes: 250 additions & 11 deletions graphql/src/schema/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::str::FromStr;

use graphql_parser::Pos;
use graphql_parser::{schema::TypeDefinition, Pos};
use inflector::Inflector;
use lazy_static::lazy_static;

Expand Down Expand Up @@ -157,16 +157,7 @@ fn add_order_by_type(
description: None,
name: type_name,
directives: vec![],
values: fields
.iter()
.map(|field| &field.name)
.map(|name| EnumValue {
position: Pos::default(),
description: None,
name: name.to_owned(),
directives: vec![],
})
.collect(),
values: field_enum_values(schema, fields)?,
});
let def = Definition::TypeDefinition(typedef);
schema.definitions.push(def);
Expand All @@ -176,6 +167,80 @@ fn add_order_by_type(
Ok(())
}

/// Generates enum values for the given set of fields.
fn field_enum_values(
schema: &Document,
fields: &[Field],
) -> Result<Vec<EnumValue>, APISchemaError> {
let mut enum_values = vec![];
for field in fields {
enum_values.push(EnumValue {
position: Pos::default(),
description: None,
name: field.name.to_owned(),
directives: vec![],
});
dotansimha marked this conversation as resolved.
Show resolved Hide resolved
enum_values.extend(field_enum_values_from_child_entity(schema, field)?);
}
Ok(enum_values)
}

fn enum_value_from_child_entity_field(
schema: &Document,
parent_field_name: &str,
field: &Field,
) -> Option<EnumValue> {
if ast::is_list_or_non_null_list_field(field) || ast::is_entity_type(schema, &field.field_type)
{
// Sorting on lists or entities is not supported.
None
} else {
Some(EnumValue {
position: Pos::default(),
description: None,
name: format!("{}__{}", parent_field_name, field.name),
directives: vec![],
})
}
}

fn field_enum_values_from_child_entity(
schema: &Document,
field: &Field,
) -> Result<Vec<EnumValue>, APISchemaError> {
fn resolve_supported_type_name(field_type: &Type) -> Option<&String> {
match field_type {
Type::NamedType(name) => Some(name),
Type::ListType(_) => None,
Type::NonNullType(of_type) => resolve_supported_type_name(of_type),
}
}

let type_name = match ENV_VARS.graphql.disable_child_sorting {
true => None,
false => resolve_supported_type_name(&field.field_type),
};

Ok(match type_name {
dotansimha marked this conversation as resolved.
Show resolved Hide resolved
Some(name) => {
let named_type = schema
.get_named_type(name)
.ok_or_else(|| APISchemaError::TypeNotFound(name.clone()))?;
match named_type {
TypeDefinition::Object(ObjectType { fields, .. })
| TypeDefinition::Interface(InterfaceType { fields, .. }) => fields
.iter()
.filter_map(|f| {
enum_value_from_child_entity_field(schema, field.name.as_str(), f)
})
.collect(),
_ => vec![],
kamilkisiela marked this conversation as resolved.
Show resolved Hide resolved
}
}
None => vec![],
})
}

/// Adds a `<type_name>_filter` enum type for the given fields to the schema.
fn add_filter_type(
schema: &mut Document,
Expand Down Expand Up @@ -887,6 +952,180 @@ mod tests {
assert_eq!(values, ["id", "name"]);
}

#[test]
fn api_schema_contains_field_order_by_enum_for_child_entity() {
let input_schema = parse_schema(
r#"
enum FurType {
NONE
FLUFFY
BRISTLY
}

type Pet {
id: ID!
name: String!
mostHatedBy: [User!]!
mostLovedBy: [User!]!
}

interface Recipe {
id: ID!
name: String!
author: User!
lovedBy: [User!]!
ingredients: [String!]!
}

type FoodRecipe implements Recipe {
id: ID!
name: String!
author: User!
ingredients: [String!]!
}

type DrinkRecipe implements Recipe {
id: ID!
name: String!
author: User!
ingredients: [String!]!
}

interface Meal {
id: ID!
name: String!
mostHatedBy: [User!]!
mostLovedBy: [User!]!
}

type Pizza implements Meal {
id: ID!
name: String!
toppings: [String!]!
mostHatedBy: [User!]!
mostLovedBy: [User!]!
}

type Burger implements Meal {
id: ID!
name: String!
bun: String!
mostHatedBy: [User!]!
mostLovedBy: [User!]!
}

type User {
id: ID!
name: String!
favoritePetNames: [String!]
pets: [Pet!]!
favoriteFurType: FurType!
favoritePet: Pet!
leastFavoritePet: Pet @derivedFrom(field: "mostHatedBy")
mostFavoritePets: [Pet!] @derivedFrom(field: "mostLovedBy")
favoriteMeal: Meal!
leastFavoriteMeal: Meal @derivedFrom(field: "mostHatedBy")
mostFavoriteMeals: [Meal!] @derivedFrom(field: "mostLovedBy")
recipes: [Recipe!]! @derivedFrom(field: "author")
}
"#,
)
.expect("Failed to parse input schema");
let schema = api_schema(&input_schema).expect("Failed to derived API schema");

let user_order_by = schema
.get_named_type("User_orderBy")
.expect("User_orderBy type is missing in derived API schema");

let enum_type = match user_order_by {
TypeDefinition::Enum(t) => Some(t),
_ => None,
}
.expect("User_orderBy type is not an enum");

let values: Vec<&str> = enum_type
.values
.iter()
.map(|value| value.name.as_str())
.collect();

assert_eq!(
values,
[
"id",
"name",
"favoritePetNames",
"pets",
"favoriteFurType",
"favoritePet",
"favoritePet__id",
"favoritePet__name",
"leastFavoritePet",
"leastFavoritePet__id",
"leastFavoritePet__name",
"mostFavoritePets",
"favoriteMeal",
"favoriteMeal__id",
"favoriteMeal__name",
"leastFavoriteMeal",
"leastFavoriteMeal__id",
"leastFavoriteMeal__name",
"mostFavoriteMeals",
"recipes",
]
);

let meal_order_by = schema
.get_named_type("Meal_orderBy")
.expect("Meal_orderBy type is missing in derived API schema");

let enum_type = match meal_order_by {
TypeDefinition::Enum(t) => Some(t),
_ => None,
}
.expect("Meal_orderBy type is not an enum");

let values: Vec<&str> = enum_type
.values
.iter()
.map(|value| value.name.as_str())
.collect();

assert_eq!(values, ["id", "name", "mostHatedBy", "mostLovedBy",]);

let recipe_order_by = schema
.get_named_type("Recipe_orderBy")
.expect("Recipe_orderBy type is missing in derived API schema");

let enum_type = match recipe_order_by {
TypeDefinition::Enum(t) => Some(t),
_ => None,
}
.expect("Recipe_orderBy type is not an enum");

let values: Vec<&str> = enum_type
.values
.iter()
.map(|value| value.name.as_str())
.collect();

assert_eq!(
values,
[
"id",
"name",
"author",
"author__id",
"author__name",
"author__favoriteFurType",
"author__favoritePet",
"author__leastFavoritePet",
"lovedBy",
"ingredients"
]
);
}

#[test]
fn api_schema_contains_object_type_filter_enum() {
let input_schema = parse_schema(
Expand Down
Loading