use crate::graphql_federation::Supergraph;
use async_graphql_parser::types::{
DocumentOperations, Field, OperationType, Selection, SelectionSet, VariableDefinition,
};
use async_graphql_parser::Positioned;
use async_graphql_value::{Name, Value};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Fetch {
pub subgraph: String,
pub query: String,
pub requires: Option<Requires>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Requires {
pub type_name: String,
pub key: Vec<String>,
pub provider: usize,
pub path: Vec<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct QueryPlan {
pub fetches: Vec<Fetch>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PlanError {
Parse(String),
NoOperation,
UnknownRootField(String),
Unsupported(&'static str),
}
struct DepFetch {
subgraph: String,
type_name: String,
key: Vec<String>,
path: Vec<String>,
selection: String,
used_vars: BTreeSet<String>,
deps: Vec<Self>,
}
pub(crate) fn plan(query: &str, sg: &Supergraph) -> Result<QueryPlan, PlanError> {
let doc =
async_graphql_parser::parse_query(query).map_err(|e| PlanError::Parse(e.to_string()))?;
let op = match &doc.operations {
DocumentOperations::Single(op) => &op.node,
DocumentOperations::Multiple(map) => {
&map.values().next().ok_or(PlanError::NoOperation)?.node
}
};
let (root_type, roots) = match op.ty {
OperationType::Query => ("Query", &sg.root_query),
OperationType::Mutation => ("Mutation", &sg.root_mutation),
OperationType::Subscription => return Err(PlanError::Unsupported("subscription")),
};
let var_types = var_type_map(&op.variable_definitions);
let mut by_subgraph: BTreeMap<String, Vec<&Field>> = BTreeMap::new();
for sel in &op.selection_set.node.items {
if let Selection::Field(field) = &sel.node {
let fname = field.node.name.node.as_str();
let owner = roots
.get(fname)
.cloned()
.ok_or_else(|| PlanError::UnknownRootField(fname.to_string()))?;
by_subgraph.entry(owner).or_default().push(&field.node);
}
}
let mut fetches = Vec::new();
let mut queue: VecDeque<(DepFetch, usize)> = VecDeque::new();
for (subgraph, fields) in by_subgraph {
let idx = fetches.len();
let mut used = BTreeSet::new();
let mut body = String::new();
for field in fields {
let (text, deps) = plan_field(sg, field, root_type, &subgraph, &mut used);
body.push_str(&text);
body.push(' ');
for d in deps {
queue.push_back((d, idx));
}
}
fetches.push(Fetch {
subgraph,
query: build_root_operation(root_type, &used, &var_types, &body),
requires: None,
});
}
while let Some((dep, provider)) = queue.pop_front() {
let idx = fetches.len();
fetches.push(Fetch {
subgraph: dep.subgraph,
query: entity_fetch_query(&dep.type_name, &dep.selection, &dep.used_vars, &var_types),
requires: Some(Requires {
type_name: dep.type_name,
key: dep.key,
provider,
path: dep.path,
}),
});
for d in dep.deps {
queue.push_back((d, idx));
}
}
Ok(QueryPlan { fetches })
}
fn plan_selection(
sg: &Supergraph,
sel_set: &SelectionSet,
parent_type: &str,
subgraph: &str,
used: &mut BTreeSet<String>,
) -> (String, Vec<DepFetch>) {
let mut local = String::from("{ ");
let mut deps = Vec::new();
let is_entity = sg.entities.contains_key(parent_type);
let mut key_injected = false;
for sel in &sel_set.items {
let Selection::Field(field) = &sel.node else {
continue; };
let field = &field.node;
match owner_of(sg, parent_type, field.name.node.as_str(), subgraph) {
Some(owner) if owner != subgraph && is_entity => {
if !key_injected {
local.push_str("__typename ");
for k in &sg.entities[parent_type].key {
local.push_str(k);
local.push(' ');
}
key_injected = true;
}
let mut dep_used = BTreeSet::new();
let (selection, nested) = plan_field(sg, field, parent_type, &owner, &mut dep_used);
deps.push(DepFetch {
subgraph: owner,
type_name: parent_type.to_string(),
key: sg.entities[parent_type].key.clone(),
path: Vec::new(),
selection,
used_vars: dep_used,
deps: nested,
});
}
_ => {
let (text, field_deps) = plan_field(sg, field, parent_type, subgraph, used);
local.push_str(&text);
local.push(' ');
deps.extend(field_deps);
}
}
}
local.push('}');
(local, deps)
}
fn plan_field(
sg: &Supergraph,
field: &Field,
parent_type: &str,
subgraph: &str,
used: &mut BTreeSet<String>,
) -> (String, Vec<DepFetch>) {
let field_name = field.name.node.as_str();
let args = render_arguments(&field.arguments, used);
if field.selection_set.node.items.is_empty() {
return (format!("{field_name}{args}"), Vec::new());
}
let child_type = sg
.field_types
.get(&(parent_type.to_string(), field_name.to_string()))
.cloned()
.unwrap_or_default();
let (child_sel, child_deps) =
plan_selection(sg, &field.selection_set.node, &child_type, subgraph, used);
let deps = child_deps
.into_iter()
.map(|mut d| {
d.path.insert(0, field_name.to_string());
d
})
.collect();
(format!("{field_name}{args} {child_sel}"), deps)
}
fn render_arguments(
args: &[(Positioned<Name>, Positioned<Value>)],
used: &mut BTreeSet<String>,
) -> String {
if args.is_empty() {
return String::new();
}
let mut out = String::from("(");
for (i, (name, value)) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(name.node.as_str());
out.push_str(": ");
out.push_str(&value.node.to_string());
collect_vars(&value.node, used);
}
out.push(')');
out
}
fn collect_vars(value: &Value, used: &mut BTreeSet<String>) {
match value {
Value::Variable(name) => {
used.insert(name.to_string());
}
Value::List(items) => items.iter().for_each(|v| collect_vars(v, used)),
Value::Object(fields) => fields.values().for_each(|v| collect_vars(v, used)),
_ => {}
}
}
fn var_type_map(defs: &[Positioned<VariableDefinition>]) -> BTreeMap<String, String> {
defs.iter()
.map(|d| {
(
d.node.name.node.to_string(),
d.node.var_type.node.to_string(),
)
})
.collect()
}
fn render_var_defs(used: &BTreeSet<String>, types: &BTreeMap<String, String>) -> String {
let defs: Vec<String> = used
.iter()
.filter_map(|n| types.get(n).map(|t| format!("${n}: {t}")))
.collect();
if defs.is_empty() {
String::new()
} else {
format!("({})", defs.join(", "))
}
}
fn build_root_operation(
root_type: &str,
used: &BTreeSet<String>,
types: &BTreeMap<String, String>,
body: &str,
) -> String {
if root_type == "Query" && used.is_empty() {
return format!("{{ {body}}}");
}
let keyword = if root_type == "Mutation" {
"mutation"
} else {
"query"
};
format!("{keyword}{} {{ {body}}}", render_var_defs(used, types))
}
fn owner_of(sg: &Supergraph, parent_type: &str, field: &str, current: &str) -> Option<String> {
if parent_type == "Query" {
return sg.root_query.get(field).cloned();
}
if parent_type == "Mutation" {
return sg.root_mutation.get(field).cloned();
}
match sg
.field_owners
.get(&(parent_type.to_string(), field.to_string()))
{
Some(owners) if owners.iter().any(|o| o == current) => Some(current.to_string()),
Some(owners) => owners.first().cloned(),
None => None,
}
}
fn entity_fetch_query(
type_name: &str,
selection: &str,
used_vars: &BTreeSet<String>,
types: &BTreeMap<String, String>,
) -> String {
let extra: String = used_vars
.iter()
.filter_map(|n| types.get(n).map(|t| format!(", ${n}: {t}")))
.collect();
format!(
"query($representations:[_Any!]!{extra}){{ _entities(representations:$representations){{ ... on {type_name} {{ {selection} }} }} }}"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graphql_federation::compose;
const ACCOUNTS: &str = r#"
type Query { me: User }
type User @key(fields: "id") { id: ID! name: String }
"#;
const REVIEWS: &str = r#"
type Query { topReviews: [Review] }
type Review { id: ID! body: String }
extend type User @key(fields: "id") { id: ID! @external reviews: [Review] }
"#;
fn supergraph() -> Supergraph {
compose(&[
("accounts".into(), ACCOUNTS.into()),
("reviews".into(), REVIEWS.into()),
])
.unwrap()
}
#[test]
fn a_single_subgraph_query_is_one_fetch() {
let plan = plan("{ me { name } }", &supergraph()).unwrap();
assert_eq!(plan.fetches.len(), 1);
assert_eq!(plan.fetches[0].subgraph, "accounts");
assert!(plan.fetches[0].query.contains("me"));
assert!(plan.fetches[0].requires.is_none());
}
#[test]
fn distinct_roots_split_into_a_fetch_per_owning_subgraph() {
let plan = plan("{ me { name } topReviews { body } }", &supergraph()).unwrap();
assert_eq!(plan.fetches.len(), 2);
let subgraphs: Vec<&str> = plan.fetches.iter().map(|f| f.subgraph.as_str()).collect();
assert!(subgraphs.contains(&"accounts") && subgraphs.contains(&"reviews"));
assert!(plan.fetches.iter().all(|f| f.requires.is_none()));
}
#[test]
fn a_cross_subgraph_entity_field_becomes_a_dependent_entities_fetch() {
let plan = plan("{ me { name reviews { body } } }", &supergraph()).unwrap();
assert_eq!(plan.fetches.len(), 2);
let root = &plan.fetches[0];
assert_eq!(root.subgraph, "accounts");
assert!(root.query.contains("name"));
assert!(root.query.contains("__typename"));
assert!(root.query.contains("id"));
assert!(root.requires.is_none());
let dep = &plan.fetches[1];
assert_eq!(dep.subgraph, "reviews");
assert!(dep.query.contains("_entities"));
assert!(dep.query.contains("... on User"));
assert!(dep.query.contains("reviews"));
let req = dep.requires.as_ref().expect("dependent fetch");
assert_eq!(req.type_name, "User");
assert_eq!(req.key, vec!["id".to_string()]);
assert_eq!(req.provider, 0);
assert_eq!(req.path, vec!["me".to_string()]);
}
#[test]
fn an_unknown_root_field_is_an_error() {
assert!(matches!(
plan("{ nope }", &supergraph()),
Err(PlanError::UnknownRootField(f)) if f == "nope"
));
}
const AGENT: &str = r#"
type Query { ping: String }
type Mutation { agent(input: String): String }
"#;
fn supergraph_with_mutation() -> Supergraph {
compose(&[
("accounts".into(), ACCOUNTS.into()),
("agent".into(), AGENT.into()),
])
.unwrap()
}
#[test]
fn a_mutation_root_fetch_is_dispatched_as_a_mutation_with_its_arguments() {
let mplan = plan(
"mutation { agent(input: \"hi\") }",
&supergraph_with_mutation(),
)
.unwrap();
assert_eq!(mplan.fetches.len(), 1);
assert_eq!(mplan.fetches[0].subgraph, "agent");
assert_eq!(mplan.fetches[0].query, "mutation { agent(input: \"hi\") }");
let qplan = plan("{ me { name } }", &supergraph_with_mutation()).unwrap();
assert_eq!(qplan.fetches[0].query, "{ me { name } }");
}
#[test]
fn a_mutation_forwards_variables_and_defines_them_on_the_fetch() {
let mplan = plan(
"mutation Turn($input: AgentInput!) { agent(input: $input) }",
&supergraph_with_mutation(),
)
.unwrap();
assert_eq!(
mplan.fetches[0].query,
"mutation($input: AgentInput!) { agent(input: $input) }"
);
let q = &mplan.fetches[0].query;
assert!(
q.contains("mutation($input: AgentInput!)"),
"the fetch must define the variable it uses, got: {q}"
);
assert!(
q.contains("agent(input: $input)"),
"the argument must reference the variable, got: {q}"
);
}
const REVIEWS_ARG: &str = r#"
type Query { topReviews: [Review] }
type Review { id: ID! body: String }
extend type User @key(fields: "id") { id: ID! @external reviews(first: Int): [Review] }
"#;
#[test]
fn an_entity_fetch_carries_nested_field_arguments_and_their_variable_defs() {
let sg = compose(&[
("accounts".into(), ACCOUNTS.into()),
("reviews".into(), REVIEWS_ARG.into()),
])
.unwrap();
let plan = plan(
"query Q($n: Int){ me { reviews(first: $n) { body } } }",
&sg,
)
.unwrap();
let dep = &plan.fetches[1];
assert!(
dep.query.contains("_entities"),
"expected an entities fetch: {}",
dep.query
);
assert!(
dep.query.contains("reviews(first: $n)"),
"nested field argument must survive into the entities fetch, got: {}",
dep.query
);
assert!(
dep.query.contains("$n: Int"),
"the entities fetch must define the variable it uses, got: {}",
dep.query
);
}
#[test]
fn no_root_field_or_argument_is_ever_dropped_from_a_plan() {
let sg = supergraph_with_mutation();
let cases: &[(&str, &[&str])] = &[
("{ me { name } }", &["me", "name"]),
(
"mutation { agent(input: \"hi\") }",
&["agent", "input:", "\"hi\""],
),
(
"mutation T($x: String){ agent(input: $x) }",
&["agent", "input:", "$x"],
),
];
for (op, must_survive) in cases {
let plan = plan(op, &sg).unwrap();
let all: String = plan.fetches.iter().map(|f| f.query.as_str()).collect();
for tok in *must_survive {
assert!(
all.contains(tok),
"planning `{op}` dropped `{tok}` — emitted fetches: {all}"
);
}
}
}
#[test]
fn renders_multiple_arguments_and_variables_nested_in_lists_and_objects() {
let sg = supergraph_with_mutation();
let plan = plan(
"mutation T($n: Int, $t: Int, $o: Int){ agent(input: \"hi\", count: $n, tags: [$t], meta: {k: $o}) }",
&sg,
)
.unwrap();
let q = &plan.fetches[0].query;
assert_eq!(
*q,
"mutation($n: Int, $o: Int, $t: Int) { agent(input: \"hi\", count: $n, tags: [$t], meta: {k: $o}) }"
);
for v in ["$n: Int", "$t: Int", "$o: Int"] {
assert!(q.contains(v), "missing var def `{v}` in: {q}");
}
}
#[test]
fn a_shareable_field_the_current_subgraph_owns_stays_local() {
let accounts =
"type Query { me: User } type User @key(fields: \"id\") { id: ID! name: String @shareable }";
let reviews =
"type Query { topReviewer: User } extend type User @key(fields: \"id\") { id: ID! @external name: String @shareable }";
let sg = compose(&[
("accounts".into(), accounts.into()),
("reviews".into(), reviews.into()),
])
.unwrap();
let plan = plan("{ topReviewer { name } }", &sg).unwrap();
assert_eq!(
plan.fetches.len(),
1,
"expected `name` resolved locally (no entity jump), got: {:?}",
plan.fetches
);
assert_eq!(plan.fetches[0].subgraph, "reviews");
assert!(
plan.fetches[0].requires.is_none(),
"no dependent fetch expected"
);
}
}