Skip to content

Commit

Permalink
graph,graphql,store: nested sorting
Browse files Browse the repository at this point in the history
graphql: test orderBy enum

graphql: do not sort an interface by child-level entity

graphql: merge Object and Interface match case

graphql: Do not pass field type

store: avoid expect

Avoid format macro

Make ChildKeyDetails more expressive

Use constraint_violation macro

Sorting by child id

Require less data

Refactor EntityOrderByChild*

Support sorting by child id

Add GRAPH_GRAPHQL_DISABLE_CHILD_SORTING (false by default)
  • Loading branch information
kamilkisiela authored and dotansimha committed Feb 14, 2023
1 parent e5dd53d commit 78adcbb
Show file tree
Hide file tree
Showing 9 changed files with 1,582 additions and 84 deletions.
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,
}

/// 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![],
});
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 {
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![],
}
}
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

0 comments on commit 78adcbb

Please sign in to comment.