Skip to content

Commit 8eea029

Browse files
tnineslingclaude
andauthored
Add search state and field routing skeleton for incremental planner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 58ae26a commit 8eea029

5 files changed

Lines changed: 569 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//! Condition satisfiability: can a set of @requires / @key fields be resolved
2+
//! at a given query graph node?
3+
4+
use std::sync::Arc;
5+
6+
use petgraph::graph::NodeIndex;
7+
8+
use super::FieldRoutingSearchSpace;
9+
use crate::error::FederationError;
10+
use crate::operation::SelectionSet;
11+
use crate::schema::ValidFederationSchema;
12+
use crate::schema::position::CompositeTypeDefinitionPosition;
13+
14+
impl FieldRoutingSearchSpace {
15+
/// Schema-based check: can every field in `conditions` be found in the
16+
/// subgraph's schema at `type_pos`?
17+
pub(super) fn can_satisfy(
18+
&self,
19+
conditions: &Arc<SelectionSet>,
20+
type_pos: &CompositeTypeDefinitionPosition,
21+
_subgraph: &Arc<str>,
22+
schema: &ValidFederationSchema,
23+
) -> bool {
24+
can_satisfy_conditions(conditions, type_pos, schema)
25+
}
26+
27+
/// Graph-based check: can every field in `conditions` be resolved at
28+
/// `node` via outgoing edges?
29+
pub(super) fn conditions_resolvable_at_node(
30+
&self,
31+
node: NodeIndex,
32+
conditions: &Arc<SelectionSet>,
33+
) -> Result<bool, FederationError> {
34+
for selection in conditions.selections.values() {
35+
match selection {
36+
crate::operation::Selection::Field(field_sel) => {
37+
if self.edge_for_field(node, &field_sel.field).is_none() {
38+
return Ok(false);
39+
}
40+
}
41+
crate::operation::Selection::InlineFragment(frag_sel) => {
42+
if self
43+
.edge_for_inline_fragment(node, &frag_sel.inline_fragment)
44+
.is_none()
45+
{
46+
return Ok(false);
47+
}
48+
}
49+
}
50+
}
51+
Ok(true)
52+
}
53+
54+
/// Do any fields in `conditions` carry @requires at `node`? If so,
55+
/// the conditions cannot be resolved in-place and need their own
56+
/// entity fetch.
57+
pub(super) fn conditions_have_requires(
58+
&self,
59+
node: NodeIndex,
60+
conditions: &Arc<SelectionSet>,
61+
) -> Result<bool, FederationError> {
62+
for selection in conditions.selections.values() {
63+
if let crate::operation::Selection::Field(field_sel) = selection
64+
&& let Some(edge_idx) = self.edge_for_field(node, &field_sel.field)
65+
{
66+
let edge = self.query_graph.edge_weight(edge_idx)?;
67+
if edge.conditions.is_some() {
68+
return Ok(true);
69+
}
70+
}
71+
}
72+
Ok(false)
73+
}
74+
}
75+
76+
/// Check whether every field in `conditions` exists in the subgraph schema
77+
/// at the given type position.
78+
fn can_satisfy_conditions(
79+
conditions: &SelectionSet,
80+
type_pos: &CompositeTypeDefinitionPosition,
81+
schema: &ValidFederationSchema,
82+
) -> bool {
83+
for selection in conditions.selections.values() {
84+
match selection {
85+
crate::operation::Selection::Field(field_sel) => {
86+
let field_name = field_sel.field.name();
87+
if field_name.as_str() == "__typename" {
88+
continue;
89+
}
90+
let field_exists = type_pos
91+
.field(field_name.clone())
92+
.is_ok_and(|field_pos| field_pos.get(schema.schema()).is_ok());
93+
if !field_exists {
94+
return false;
95+
}
96+
}
97+
crate::operation::Selection::InlineFragment(frag_sel) => {
98+
if let Some(type_cond) = &frag_sel.inline_fragment.type_condition_position {
99+
let inner_type_name = type_cond.type_name();
100+
let inner_pos = schema
101+
.get_type(inner_type_name)
102+
.ok()
103+
.and_then(|ty| CompositeTypeDefinitionPosition::try_from(ty).ok());
104+
match inner_pos {
105+
Some(pos) => {
106+
if !can_satisfy_conditions(&frag_sel.selection_set, &pos, schema) {
107+
return false;
108+
}
109+
}
110+
None => return false,
111+
}
112+
}
113+
}
114+
}
115+
}
116+
true
117+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//! Field-level routing as a BULB search space.
2+
//!
3+
//! The planner walks the operation selection-by-selection ("pendings"),
4+
//! consulting the query graph for where each field can be resolved:
5+
//! - [`state`]: mutable search state (pending stack, checkpoints).
6+
//! - [`routing`]: enumerating and ranking options for a selection.
7+
//! - [`commit`]: applying a chosen option to the fetch graph.
8+
//! - [`conditions`]: condition satisfiability for @requires / @key.
9+
//! - [`requires`]: hop-edge inputs and condition paths.
10+
//!
11+
//! This file holds the search-space type and the
12+
//! [`BulbSearchSpace`] implementation.
13+
14+
mod conditions;
15+
mod requires;
16+
pub(super) mod state;
17+
18+
use std::sync::Arc;
19+
20+
use apollo_compiler::Name;
21+
use petgraph::graph::EdgeIndex;
22+
use petgraph::graph::NodeIndex;
23+
use petgraph::visit::EdgeRef;
24+
#[allow(unused_imports)]
25+
pub(crate) use state::PendingSelection;
26+
#[allow(unused_imports)]
27+
pub(crate) use state::PlanState;
28+
29+
use super::shared_path::SharedPath;
30+
use crate::error::FederationError;
31+
use crate::operation::Field;
32+
use crate::operation::InlineFragment;
33+
use crate::operation::Selection;
34+
use crate::operation::SelectionId;
35+
use crate::operation::SelectionSet;
36+
use crate::query_graph::OverrideConditions;
37+
use crate::query_graph::QueryGraph;
38+
use crate::query_graph::graph_path::operation::OpPathElement;
39+
use crate::schema::ValidFederationSchema;
40+
use crate::schema::position::CompositeTypeDefinitionPosition;
41+
42+
/// Cache key for routing options. Captures the selection identity at a QG node.
43+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44+
pub(super) enum RoutingCacheKey {
45+
Field(Name),
46+
InlineFragment(Option<Name>),
47+
}
48+
49+
/// Subgraph, type position, and schema at a query graph node.
50+
pub(super) struct NodeSource {
51+
pub(super) subgraph: Arc<str>,
52+
pub(super) type_pos: CompositeTypeDefinitionPosition,
53+
pub(super) schema: ValidFederationSchema,
54+
}
55+
56+
/// Search space presenting field-level routing decisions as a BULB problem.
57+
pub(crate) struct FieldRoutingSearchSpace {
58+
pub(crate) query_graph: Arc<QueryGraph>,
59+
pub(crate) supergraph_schema: ValidFederationSchema,
60+
pub(crate) override_conditions: OverrideConditions,
61+
pub(crate) inconsistent_abstract_types: Arc<apollo_compiler::collections::IndexSet<Name>>,
62+
}
63+
64+
impl FieldRoutingSearchSpace {
65+
pub(super) fn node_source(&self, node: NodeIndex) -> Result<NodeSource, FederationError> {
66+
let data = self.query_graph.node_weight(node)?;
67+
Ok(NodeSource {
68+
subgraph: data.source.clone(),
69+
type_pos: data.type_.clone().try_into()?,
70+
schema: self.query_graph.schema_by_source(&data.source)?.clone(),
71+
})
72+
}
73+
74+
/// Select `__typename` in `fetch_node` at `base_path` so the executor
75+
/// can identify the concrete type for entity representations.
76+
pub(super) fn append_typename(
77+
&self,
78+
state: &mut PlanState,
79+
fetch_node: NodeIndex,
80+
base_path: &SharedPath<Arc<OpPathElement>>,
81+
source: &NodeSource,
82+
) {
83+
let typename = Arc::new(OpPathElement::Field(Field::new_introspection_typename(
84+
&source.schema,
85+
&source.type_pos,
86+
None,
87+
)));
88+
state
89+
.graph
90+
.append_selection(fetch_node, &base_path.pushed(typename), None);
91+
}
92+
93+
/// Op path at which selections enter an entity fetch group: entity
94+
/// fetches start from the `_Entity` union, so everything nests under a
95+
/// `... on <ConcreteType>` condition rebased onto the supergraph schema
96+
/// (which OpPaths reference).
97+
pub(super) fn entity_root_path(
98+
&self,
99+
type_name: &Name,
100+
) -> Result<SharedPath<Arc<OpPathElement>>, FederationError> {
101+
let rebased: CompositeTypeDefinitionPosition =
102+
self.supergraph_schema.get_type(type_name)?.try_into()?;
103+
let condition = InlineFragment {
104+
schema: self.supergraph_schema.clone(),
105+
parent_type_position: rebased.clone(),
106+
type_condition_position: Some(rebased),
107+
directives: Default::default(),
108+
selection_id: SelectionId::new(),
109+
};
110+
Ok(SharedPath::new().pushed(Arc::new(OpPathElement::InlineFragment(condition))))
111+
}
112+
113+
/// Can condition fields simply be selected in the fetch at `node`?
114+
/// True when the subgraph resolves every field itself and none carries
115+
/// @requires (which draws on an entity representation and needs its own
116+
/// fetch). The graph-based check complements the schema-based one:
117+
/// @external fields may still resolve at `node` when it is a
118+
/// provides-copy created by an ancestor's @provides.
119+
pub(super) fn can_resolve_in_place(
120+
&self,
121+
node: NodeIndex,
122+
conditions: &Arc<SelectionSet>,
123+
source: &NodeSource,
124+
) -> Result<bool, FederationError> {
125+
let satisfiable = self.can_satisfy(
126+
conditions,
127+
&source.type_pos,
128+
&source.subgraph,
129+
&source.schema,
130+
) || self.conditions_resolvable_at_node(node, conditions)?;
131+
Ok(satisfiable && !self.conditions_have_requires(node, conditions)?)
132+
}
133+
134+
/// Outgoing edge indices from a query graph node, sorted and filtered.
135+
pub(super) fn out_edge_indices(&self, node: NodeIndex) -> Vec<EdgeIndex> {
136+
self.query_graph
137+
.out_edges(node)
138+
.into_iter()
139+
.map(|edge_ref| edge_ref.id())
140+
.collect()
141+
}
142+
143+
/// Find the outgoing edge for a field at a query graph node.
144+
pub(super) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option<EdgeIndex> {
145+
self.query_graph
146+
.edge_for_field(node, field, &self.override_conditions)
147+
}
148+
149+
/// Find the outgoing downcast edge for an inline fragment at a query
150+
/// graph node.
151+
pub(super) fn edge_for_inline_fragment(
152+
&self,
153+
node: NodeIndex,
154+
fragment: &InlineFragment,
155+
) -> Option<EdgeIndex> {
156+
self.query_graph.edge_for_inline_fragment(node, fragment)
157+
}
158+
}
159+
160+
/// Short human-readable label for a selection, for logging.
161+
pub(super) fn selection_label(selection: &Selection) -> String {
162+
match selection {
163+
Selection::Field(f) => f.field.field_position.to_string(),
164+
Selection::InlineFragment(f) => {
165+
format!("... on {:?}", f.inline_fragment.type_condition_position)
166+
}
167+
}
168+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! Key-hop input path helpers: building the unconditioned input path and
2+
//! extracting trailing condition fragments for entity fetch op paths.
3+
4+
use std::sync::Arc;
5+
6+
use super::super::shared_path::SharedPath;
7+
use crate::operation::DirectiveList;
8+
use crate::query_graph::graph_path::operation::OpPathElement;
9+
10+
/// The trailing inline-fragment elements of `op_path` (after the last field)
11+
/// that carry directives: @skip/@include conditions at the current position,
12+
/// which a key hop must carry into the entity fetch's op path or the hopped
13+
/// selections lose their gating.
14+
pub(super) fn trailing_condition_fragments(
15+
op_path: &SharedPath<Arc<OpPathElement>>,
16+
) -> Vec<Arc<OpPathElement>> {
17+
let mut trailing = Vec::new();
18+
for element in op_path.iter() {
19+
match element.as_ref() {
20+
OpPathElement::Field(_) => trailing.clear(),
21+
OpPathElement::InlineFragment(frag) => {
22+
if !frag.directives.is_empty() {
23+
trailing.push(element.clone());
24+
}
25+
}
26+
}
27+
}
28+
trailing
29+
}
30+
31+
/// `op_path` with @skip/@include stripped from its inline-fragment elements,
32+
/// for appending key input selections. Inputs must be selected
33+
/// unconditionally: an input gated by one branch's Boolean condition leaves
34+
/// the representation incomplete whenever a different branch executes.
35+
/// Condition-only fragments are dropped; type-conditioned fragments keep the
36+
/// downcast without the conditions.
37+
pub(super) fn unconditioned_input_path(
38+
op_path: &SharedPath<Arc<OpPathElement>>,
39+
) -> SharedPath<Arc<OpPathElement>> {
40+
let mut elements = Vec::with_capacity(op_path.len());
41+
for element in op_path.iter() {
42+
match element.as_ref() {
43+
OpPathElement::Field(_) => elements.push(element.clone()),
44+
OpPathElement::InlineFragment(frag) => {
45+
let stripped: DirectiveList = frag
46+
.directives
47+
.iter()
48+
.filter(|d| d.name != "skip" && d.name != "include")
49+
.cloned()
50+
.collect();
51+
if stripped.len() == frag.directives.len() {
52+
elements.push(element.clone());
53+
} else if !stripped.is_empty() || frag.type_condition_position.is_some() {
54+
elements.push(Arc::new(OpPathElement::InlineFragment(
55+
frag.with_updated_directives(stripped),
56+
)));
57+
}
58+
}
59+
}
60+
}
61+
SharedPath::from_vec(elements)
62+
}

0 commit comments

Comments
 (0)