Skip to content

Commit 78adcbb

Browse files
kamilkisieladotansimha
authored andcommitted
graph,graphql,store: nested sorting
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)
1 parent e5dd53d commit 78adcbb

File tree

9 files changed

+1582
-84
lines changed

9 files changed

+1582
-84
lines changed

docs/environment-variables.md

+3
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ those.
126126
- `GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS`: disables the ability to use AND/OR
127127
filters. This is useful if we want to disable filters because of
128128
performance reasons.
129+
- `GRAPH_GRAPHQL_DISABLE_CHILD_SORTING`: disables the ability to use child-based
130+
sorting. This is useful if we want to disable child-based sorting because of
131+
performance reasons.
129132
- `GRAPH_GRAPHQL_TRACE_TOKEN`: the token to use to enable query tracing for
130133
a GraphQL request. If this is set, requests that have a header
131134
`X-GraphTraceQuery` set to this value will include a trace of the SQL

graph/src/components/store/mod.rs

+22
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,35 @@ impl EntityFilter {
280280
}
281281
}
282282

283+
/// Holds the information needed to query a store.
284+
#[derive(Clone, Debug, PartialEq)]
285+
pub struct EntityOrderByChildInfo {
286+
/// The attribute of the child entity that is used to order the results.
287+
pub sort_by_attribute: Attribute,
288+
/// The attribute that is used to join to the parent and child entity.
289+
pub join_attribute: Attribute,
290+
/// If true, the child entity is derived from the parent entity.
291+
pub derived: bool,
292+
}
293+
294+
/// Holds the information needed to order the results of a query based on nested entities.
295+
#[derive(Clone, Debug, PartialEq)]
296+
pub enum EntityOrderByChild {
297+
Object(EntityOrderByChildInfo, EntityType),
298+
Interface(EntityOrderByChildInfo, Vec<EntityType>),
299+
}
300+
283301
/// The order in which entities should be restored from a store.
284302
#[derive(Clone, Debug, PartialEq)]
285303
pub enum EntityOrder {
286304
/// Order ascending by the given attribute. Use `id` as a tie-breaker
287305
Ascending(String, ValueType),
288306
/// Order descending by the given attribute. Use `id` as a tie-breaker
289307
Descending(String, ValueType),
308+
/// Order ascending by the given attribute of a child entity. Use `id` as a tie-breaker
309+
ChildAscending(EntityOrderByChild),
310+
/// Order descending by the given attribute of a child entity. Use `id` as a tie-breaker
311+
ChildDescending(EntityOrderByChild),
290312
/// Order by the `id` of the entities
291313
Default,
292314
/// Do not order at all. This speeds up queries where we know that

graph/src/env/graphql.rs

+6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ pub struct EnvVarsGraphQl {
8787
/// Set by the flag `GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS`. Off by default.
8888
/// Disables AND/OR filters
8989
pub disable_bool_filters: bool,
90+
/// Set by the flag `GRAPH_GRAPHQL_DISABLE_CHILD_SORTING`. Off by default.
91+
/// Disables child-based sorting
92+
pub disable_child_sorting: bool,
9093
/// Set by `GRAPH_GRAPHQL_TRACE_TOKEN`, the token to use to enable query
9194
/// tracing for a GraphQL request. If this is set, requests that have a
9295
/// header `X-GraphTraceQuery` set to this value will include a trace of
@@ -137,6 +140,7 @@ impl From<InnerGraphQl> for EnvVarsGraphQl {
137140
error_result_size: x.error_result_size.0 .0,
138141
max_operations_per_connection: x.max_operations_per_connection,
139142
disable_bool_filters: x.disable_bool_filters.0,
143+
disable_child_sorting: x.disable_child_sorting.0,
140144
query_trace_token: x.query_trace_token,
141145
}
142146
}
@@ -185,6 +189,8 @@ pub struct InnerGraphQl {
185189
max_operations_per_connection: usize,
186190
#[envconfig(from = "GRAPH_GRAPHQL_DISABLE_BOOL_FILTERS", default = "false")]
187191
pub disable_bool_filters: EnvVarBoolean,
192+
#[envconfig(from = "GRAPH_GRAPHQL_DISABLE_CHILD_SORTING", default = "false")]
193+
pub disable_child_sorting: EnvVarBoolean,
188194
#[envconfig(from = "GRAPH_GRAPHQL_TRACE_TOKEN", default = "")]
189195
query_trace_token: String,
190196
}

graph/src/lib.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,11 @@ pub mod prelude {
122122
pub use crate::components::store::{
123123
AttributeNames, BlockNumber, CachedEthereumCall, ChainStore, Child, ChildMultiplicity,
124124
EntityCache, EntityChange, EntityChangeOperation, EntityCollection, EntityFilter,
125-
EntityLink, EntityModification, EntityOperation, EntityOrder, EntityQuery, EntityRange,
126-
EntityWindow, EthereumCallCache, ParentLink, PartialBlockPtr, PoolWaitStats, QueryStore,
127-
QueryStoreManager, StoreError, StoreEvent, StoreEventStream, StoreEventStreamBox,
128-
SubgraphStore, UnfailOutcome, WindowAttribute, BLOCK_NUMBER_MAX,
125+
EntityLink, EntityModification, EntityOperation, EntityOrder, EntityOrderByChild,
126+
EntityOrderByChildInfo, EntityQuery, EntityRange, EntityWindow, EthereumCallCache,
127+
ParentLink, PartialBlockPtr, PoolWaitStats, QueryStore, QueryStoreManager, StoreError,
128+
StoreEvent, StoreEventStream, StoreEventStreamBox, SubgraphStore, UnfailOutcome,
129+
WindowAttribute, BLOCK_NUMBER_MAX,
129130
};
130131
pub use crate::components::subgraph::{
131132
BlockState, DataSourceTemplateInfo, HostMetrics, RuntimeHost, RuntimeHostBuilder,

graphql/src/schema/api.rs

+250-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::str::FromStr;
22

3-
use graphql_parser::Pos;
3+
use graphql_parser::{schema::TypeDefinition, Pos};
44
use inflector::Inflector;
55
use lazy_static::lazy_static;
66

@@ -157,16 +157,7 @@ fn add_order_by_type(
157157
description: None,
158158
name: type_name,
159159
directives: vec![],
160-
values: fields
161-
.iter()
162-
.map(|field| &field.name)
163-
.map(|name| EnumValue {
164-
position: Pos::default(),
165-
description: None,
166-
name: name.to_owned(),
167-
directives: vec![],
168-
})
169-
.collect(),
160+
values: field_enum_values(schema, fields)?,
170161
});
171162
let def = Definition::TypeDefinition(typedef);
172163
schema.definitions.push(def);
@@ -176,6 +167,80 @@ fn add_order_by_type(
176167
Ok(())
177168
}
178169

170+
/// Generates enum values for the given set of fields.
171+
fn field_enum_values(
172+
schema: &Document,
173+
fields: &[Field],
174+
) -> Result<Vec<EnumValue>, APISchemaError> {
175+
let mut enum_values = vec![];
176+
for field in fields {
177+
enum_values.push(EnumValue {
178+
position: Pos::default(),
179+
description: None,
180+
name: field.name.to_owned(),
181+
directives: vec![],
182+
});
183+
enum_values.extend(field_enum_values_from_child_entity(schema, field)?);
184+
}
185+
Ok(enum_values)
186+
}
187+
188+
fn enum_value_from_child_entity_field(
189+
schema: &Document,
190+
parent_field_name: &str,
191+
field: &Field,
192+
) -> Option<EnumValue> {
193+
if ast::is_list_or_non_null_list_field(field) || ast::is_entity_type(schema, &field.field_type)
194+
{
195+
// Sorting on lists or entities is not supported.
196+
None
197+
} else {
198+
Some(EnumValue {
199+
position: Pos::default(),
200+
description: None,
201+
name: format!("{}__{}", parent_field_name, field.name),
202+
directives: vec![],
203+
})
204+
}
205+
}
206+
207+
fn field_enum_values_from_child_entity(
208+
schema: &Document,
209+
field: &Field,
210+
) -> Result<Vec<EnumValue>, APISchemaError> {
211+
fn resolve_supported_type_name(field_type: &Type) -> Option<&String> {
212+
match field_type {
213+
Type::NamedType(name) => Some(name),
214+
Type::ListType(_) => None,
215+
Type::NonNullType(of_type) => resolve_supported_type_name(of_type),
216+
}
217+
}
218+
219+
let type_name = match ENV_VARS.graphql.disable_child_sorting {
220+
true => None,
221+
false => resolve_supported_type_name(&field.field_type),
222+
};
223+
224+
Ok(match type_name {
225+
Some(name) => {
226+
let named_type = schema
227+
.get_named_type(name)
228+
.ok_or_else(|| APISchemaError::TypeNotFound(name.clone()))?;
229+
match named_type {
230+
TypeDefinition::Object(ObjectType { fields, .. })
231+
| TypeDefinition::Interface(InterfaceType { fields, .. }) => fields
232+
.iter()
233+
.filter_map(|f| {
234+
enum_value_from_child_entity_field(schema, field.name.as_str(), f)
235+
})
236+
.collect(),
237+
_ => vec![],
238+
}
239+
}
240+
None => vec![],
241+
})
242+
}
243+
179244
/// Adds a `<type_name>_filter` enum type for the given fields to the schema.
180245
fn add_filter_type(
181246
schema: &mut Document,
@@ -887,6 +952,180 @@ mod tests {
887952
assert_eq!(values, ["id", "name"]);
888953
}
889954

955+
#[test]
956+
fn api_schema_contains_field_order_by_enum_for_child_entity() {
957+
let input_schema = parse_schema(
958+
r#"
959+
enum FurType {
960+
NONE
961+
FLUFFY
962+
BRISTLY
963+
}
964+
965+
type Pet {
966+
id: ID!
967+
name: String!
968+
mostHatedBy: [User!]!
969+
mostLovedBy: [User!]!
970+
}
971+
972+
interface Recipe {
973+
id: ID!
974+
name: String!
975+
author: User!
976+
lovedBy: [User!]!
977+
ingredients: [String!]!
978+
}
979+
980+
type FoodRecipe implements Recipe {
981+
id: ID!
982+
name: String!
983+
author: User!
984+
ingredients: [String!]!
985+
}
986+
987+
type DrinkRecipe implements Recipe {
988+
id: ID!
989+
name: String!
990+
author: User!
991+
ingredients: [String!]!
992+
}
993+
994+
interface Meal {
995+
id: ID!
996+
name: String!
997+
mostHatedBy: [User!]!
998+
mostLovedBy: [User!]!
999+
}
1000+
1001+
type Pizza implements Meal {
1002+
id: ID!
1003+
name: String!
1004+
toppings: [String!]!
1005+
mostHatedBy: [User!]!
1006+
mostLovedBy: [User!]!
1007+
}
1008+
1009+
type Burger implements Meal {
1010+
id: ID!
1011+
name: String!
1012+
bun: String!
1013+
mostHatedBy: [User!]!
1014+
mostLovedBy: [User!]!
1015+
}
1016+
1017+
type User {
1018+
id: ID!
1019+
name: String!
1020+
favoritePetNames: [String!]
1021+
pets: [Pet!]!
1022+
favoriteFurType: FurType!
1023+
favoritePet: Pet!
1024+
leastFavoritePet: Pet @derivedFrom(field: "mostHatedBy")
1025+
mostFavoritePets: [Pet!] @derivedFrom(field: "mostLovedBy")
1026+
favoriteMeal: Meal!
1027+
leastFavoriteMeal: Meal @derivedFrom(field: "mostHatedBy")
1028+
mostFavoriteMeals: [Meal!] @derivedFrom(field: "mostLovedBy")
1029+
recipes: [Recipe!]! @derivedFrom(field: "author")
1030+
}
1031+
"#,
1032+
)
1033+
.expect("Failed to parse input schema");
1034+
let schema = api_schema(&input_schema).expect("Failed to derived API schema");
1035+
1036+
let user_order_by = schema
1037+
.get_named_type("User_orderBy")
1038+
.expect("User_orderBy type is missing in derived API schema");
1039+
1040+
let enum_type = match user_order_by {
1041+
TypeDefinition::Enum(t) => Some(t),
1042+
_ => None,
1043+
}
1044+
.expect("User_orderBy type is not an enum");
1045+
1046+
let values: Vec<&str> = enum_type
1047+
.values
1048+
.iter()
1049+
.map(|value| value.name.as_str())
1050+
.collect();
1051+
1052+
assert_eq!(
1053+
values,
1054+
[
1055+
"id",
1056+
"name",
1057+
"favoritePetNames",
1058+
"pets",
1059+
"favoriteFurType",
1060+
"favoritePet",
1061+
"favoritePet__id",
1062+
"favoritePet__name",
1063+
"leastFavoritePet",
1064+
"leastFavoritePet__id",
1065+
"leastFavoritePet__name",
1066+
"mostFavoritePets",
1067+
"favoriteMeal",
1068+
"favoriteMeal__id",
1069+
"favoriteMeal__name",
1070+
"leastFavoriteMeal",
1071+
"leastFavoriteMeal__id",
1072+
"leastFavoriteMeal__name",
1073+
"mostFavoriteMeals",
1074+
"recipes",
1075+
]
1076+
);
1077+
1078+
let meal_order_by = schema
1079+
.get_named_type("Meal_orderBy")
1080+
.expect("Meal_orderBy type is missing in derived API schema");
1081+
1082+
let enum_type = match meal_order_by {
1083+
TypeDefinition::Enum(t) => Some(t),
1084+
_ => None,
1085+
}
1086+
.expect("Meal_orderBy type is not an enum");
1087+
1088+
let values: Vec<&str> = enum_type
1089+
.values
1090+
.iter()
1091+
.map(|value| value.name.as_str())
1092+
.collect();
1093+
1094+
assert_eq!(values, ["id", "name", "mostHatedBy", "mostLovedBy",]);
1095+
1096+
let recipe_order_by = schema
1097+
.get_named_type("Recipe_orderBy")
1098+
.expect("Recipe_orderBy type is missing in derived API schema");
1099+
1100+
let enum_type = match recipe_order_by {
1101+
TypeDefinition::Enum(t) => Some(t),
1102+
_ => None,
1103+
}
1104+
.expect("Recipe_orderBy type is not an enum");
1105+
1106+
let values: Vec<&str> = enum_type
1107+
.values
1108+
.iter()
1109+
.map(|value| value.name.as_str())
1110+
.collect();
1111+
1112+
assert_eq!(
1113+
values,
1114+
[
1115+
"id",
1116+
"name",
1117+
"author",
1118+
"author__id",
1119+
"author__name",
1120+
"author__favoriteFurType",
1121+
"author__favoritePet",
1122+
"author__leastFavoritePet",
1123+
"lovedBy",
1124+
"ingredients"
1125+
]
1126+
);
1127+
}
1128+
8901129
#[test]
8911130
fn api_schema_contains_object_type_filter_enum() {
8921131
let input_schema = parse_schema(

0 commit comments

Comments
 (0)