use graphql_tools::parser::query as query_ast;
use serde::{ser::SerializeSeq, Deserialize, Serialize};
use std::{
collections::BTreeSet,
fmt::{Debug, Display},
hash::Hash,
};
use crate::query_planner::{
ast::merge_path::{Condition, MergePath, Segment},
ast::value::Value,
utils::pretty_display::{get_indent, PrettyDisplay},
};
use super::{arguments::ArgumentsMap, selection_item::SelectionItem};
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SelectionSet {
pub items: Vec<SelectionItem>,
}
impl<'a, T: query_ast::Text<'a>> From<query_ast::SelectionSet<'a, T>> for SelectionSet {
fn from(selection_set: query_ast::SelectionSet<'a, T>) -> Self {
Self {
items: selection_set
.items
.into_iter()
.map(|item| item.into())
.collect::<Vec<SelectionItem>>(),
}
}
}
impl PartialEq for SelectionSet {
fn eq(&self, other: &Self) -> bool {
self.items == other.items
}
}
impl Eq for SelectionSet {}
impl Display for SelectionSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.items.is_empty() {
return Ok(());
}
write!(f, "{{")?;
for (i, item) in self.items.iter().enumerate() {
if i + 1 == self.items.len() {
write!(f, "{}", item)?;
} else {
write!(f, "{} ", item)?;
}
}
write!(f, "}}")?;
Ok(())
}
}
impl SelectionSet {
pub fn cost(&self) -> u64 {
let mut cost = 1;
for node in &self.items {
cost += node.cost();
}
cost
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn contains(&self, other: &Self) -> bool {
selection_items_are_subset_of(&self.items, &other.items)
}
pub fn variable_usages(&self) -> BTreeSet<String> {
self.items
.iter()
.flat_map(|item| item.variable_usages())
.collect()
}
pub fn strip_for_plan_input(&self) -> Self {
SelectionSet {
items: self
.items
.iter()
.map(|item| item.strip_for_plan_input())
.collect(),
}
}
pub fn entities_field(&self) -> Option<&FieldSelection> {
self.items.iter().find_map(|item| {
if let SelectionItem::Field(field) = item {
if field.name == "_entities" {
return Some(field);
}
}
None
})
}
pub fn iter_fields_and_fragments_of_same_type<'a>(
&'a self,
type_name: &'a str,
) -> impl Iterator<Item = &'a FieldSelection> + 'a {
let mut stack = vec![self.items.iter()];
std::iter::from_fn(move || loop {
let iter = stack.last_mut()?;
match iter.next() {
Some(SelectionItem::Field(field)) => {
return Some(field);
}
Some(SelectionItem::InlineFragment(fragment))
if fragment.type_condition == type_name =>
{
stack.push(fragment.selections.items.iter());
}
Some(_) => {}
None => {
stack.pop();
}
}
})
}
}
impl Hash for SelectionSet {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.items.hash(state);
}
}
impl Serialize for SelectionSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.items.len()))?;
for e in &self.items {
seq.serialize_element(&e)?;
}
seq.end()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct FieldSelection {
pub name: String,
#[serde(skip_serializing_if = "SelectionSet::is_empty")]
pub selections: SelectionSet,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<ArgumentsMap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_if: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_if: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub omit_from_response: bool,
}
impl Hash for FieldSelection {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
if let Some(alias) = &self.alias {
alias.hash(state);
}
self.selections.hash(state);
if let Some(arguments) = &self.arguments {
arguments.hash(state);
}
}
}
impl PartialEq for FieldSelection {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.alias == other.alias
&& self.arguments() == other.arguments()
}
}
impl FieldSelection {
pub fn with_new_selections(&self, selections: SelectionSet) -> Self {
FieldSelection {
name: self.name.clone(),
alias: self.alias.clone(),
selections,
arguments: self.arguments.clone(),
skip_if: self.skip_if.clone(),
include_if: self.include_if.clone(),
omit_from_response: self.omit_from_response,
}
}
pub fn selection_identifier(&self) -> &str {
match &self.alias {
Some(alias) => alias,
None => &self.name,
}
}
pub fn arguments_hash(&self) -> u64 {
if let Some(arguments) = &self.arguments {
return arguments.hash_u64();
}
0
}
pub fn is_leaf(&self) -> bool {
self.selections.is_empty()
}
pub fn new_typename() -> Self {
FieldSelection {
name: "__typename".to_string(),
alias: None,
selections: SelectionSet::default(),
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
}
}
pub fn new_skipped_typename() -> Self {
FieldSelection {
name: "__typename".to_string(),
alias: None,
selections: SelectionSet::default(),
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: true,
}
}
pub fn variable_usages(&self) -> BTreeSet<String> {
let mut usages = BTreeSet::new();
if let Some(arguments) = &self.arguments {
for value in arguments.values() {
usages.extend(value.variable_usages());
}
}
if let Some(include_if) = &self.include_if {
usages.insert(include_if.clone());
}
if let Some(skip_if) = &self.skip_if {
usages.insert(skip_if.clone());
}
usages.extend(self.selections.variable_usages());
usages
}
pub fn arguments(&self) -> Option<&ArgumentsMap> {
match &self.arguments {
Some(arguments) => {
if arguments.is_empty() {
None
} else {
Some(arguments)
}
}
None => None,
}
}
pub fn is_introspection_field(&self) -> bool {
self.name.starts_with("__")
}
pub fn representations_variable_name(&self) -> Option<&str> {
let value = self.arguments.as_ref()?.get_argument("representations")?;
match value {
Value::Variable(variable_name) => Some(variable_name.as_str()),
_ => None,
}
}
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InlineFragmentSelection {
pub type_condition: String,
pub selections: SelectionSet,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_if: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_if: Option<String>,
}
impl InlineFragmentSelection {
pub fn variable_usages(&self) -> BTreeSet<String> {
let mut usages = BTreeSet::new();
if let Some(include_if) = &self.include_if {
usages.insert(include_if.clone());
}
if let Some(skip_if) = &self.skip_if {
usages.insert(skip_if.clone());
}
usages.extend(self.selections.variable_usages());
usages
}
pub fn with_new_selections(&self, selections: SelectionSet) -> Self {
InlineFragmentSelection {
type_condition: self.type_condition.clone(),
selections,
skip_if: self.skip_if.clone(),
include_if: self.include_if.clone(),
}
}
}
impl Hash for InlineFragmentSelection {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.type_condition.hash(state);
self.selections.hash(state);
}
}
impl Display for FieldSelection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(alias) = &self.alias {
write!(f, "{}: ", alias)?;
}
write!(f, "{}", self.name)?;
if let Some(arguments) = &self.arguments() {
write!(f, "({})", arguments)?;
}
if let Some(skip_if) = &self.skip_if {
write!(f, " @skip(if: ${})", skip_if)?;
}
if let Some(include_if) = &self.include_if {
write!(f, " @include(if: ${})", include_if)?;
}
write!(f, "{}", self.selections)
}
}
impl PrettyDisplay for FieldSelection {
fn pretty_fmt(&self, f: &mut std::fmt::Formatter<'_>, depth: usize) -> std::fmt::Result {
let indent = get_indent(depth);
let alias_str = match &self.alias {
Some(alias_name) => format!("{}: ", alias_name),
None => String::new(),
};
let args_str = match &self.arguments() {
Some(arguments) => format!("({})", arguments),
None => String::new(),
};
write!(f, "{indent}{}{}{}", alias_str, self.name, args_str)?;
if let Some(skip_if) = &self.skip_if {
write!(f, " @skip(if: ${})", skip_if)?;
}
if let Some(include_if) = &self.include_if {
write!(f, " @include(if: ${})", include_if)?;
}
if self.is_leaf() {
return writeln!(f);
}
writeln!(f, " {{")?;
self.selections.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}}")
}
}
impl PrettyDisplay for SelectionSet {
fn pretty_fmt(&self, f: &mut std::fmt::Formatter<'_>, depth: usize) -> std::fmt::Result {
for item in self.items.iter() {
item.pretty_fmt(f, depth)?;
}
Ok(())
}
}
impl Display for InlineFragmentSelection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "...on {}", self.type_condition)?;
if let Some(skip_if) = &self.skip_if {
write!(f, " @skip(if: ${})", skip_if)?;
}
if let Some(include_if) = &self.include_if {
write!(f, " @include(if: ${})", include_if)?;
}
write!(f, "{}", self.selections)
}
}
impl PrettyDisplay for InlineFragmentSelection {
fn pretty_fmt(&self, f: &mut std::fmt::Formatter<'_>, depth: usize) -> std::fmt::Result {
let indent = get_indent(depth);
write!(f, "{indent}... on {} ", self.type_condition)?;
if let Some(skip_if) = &self.skip_if {
write!(f, "@skip(if: ${}) ", skip_if)?;
}
if let Some(include_if) = &self.include_if {
write!(f, "@include(if: ${}) ", include_if)?;
}
writeln!(f, "{{")?;
self.selections.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}}")
}
}
pub fn selection_items_are_subset_of(source: &[SelectionItem], target: &[SelectionItem]) -> bool {
target.iter().all(|target_node| {
source
.iter()
.any(|source_node| selection_item_is_subset_of(source_node, target_node))
})
}
fn selection_item_is_subset_of(source: &SelectionItem, target: &SelectionItem) -> bool {
match (source, target) {
(SelectionItem::Field(source_field), SelectionItem::Field(target_field)) => {
if source_field.name != target_field.name {
return false;
}
if source_field.is_leaf() != target_field.is_leaf() {
return false;
}
selection_items_are_subset_of(
&source_field.selections.items,
&target_field.selections.items,
)
}
_ => false,
}
}
pub fn merge_selection_set(target: &mut SelectionSet, source: &SelectionSet, as_first: bool) {
if source.items.is_empty() {
return;
}
let mut pending_items = Vec::with_capacity(source.items.len());
for source_item in source.items.iter() {
let mut found = false;
for target_item in target.items.iter_mut() {
match (source_item, target_item) {
(SelectionItem::Field(source_field), SelectionItem::Field(target_field))
if source_field == target_field
&& field_condition_equal(
&Option::<Condition>::from(source_field),
target_field,
) =>
{
found = true;
merge_field_omit_from_response(target_field, source_field);
merge_selection_set(
&mut target_field.selections,
&source_field.selections,
as_first,
);
break;
}
(
SelectionItem::InlineFragment(source_fragment),
SelectionItem::InlineFragment(target_fragment),
) if source_fragment.type_condition == target_fragment.type_condition
&& fragment_condition_equal(
&Option::<Condition>::from(source_fragment),
target_fragment,
) =>
{
found = true;
merge_selection_set(
&mut target_fragment.selections,
&source_fragment.selections,
as_first,
);
break;
}
_ => {}
}
}
if !found {
pending_items.push(source_item.clone())
}
}
if !pending_items.is_empty() {
if as_first {
let mut new_items = pending_items;
new_items.append(&mut target.items);
target.items = new_items;
} else {
target.items.extend(pending_items);
}
}
}
#[inline]
fn merge_field_omit_from_response(target: &mut FieldSelection, source: &FieldSelection) {
if target.name == "__typename" {
target.omit_from_response &= source.omit_from_response;
}
}
pub fn find_selection_set_by_path_mut<'a>(
root_selection_set: &'a mut SelectionSet,
path: &MergePath,
) -> Option<&'a mut SelectionSet> {
let mut current_selection_set = root_selection_set;
for path_element in path.inner.iter() {
match path_element {
Segment::List => {
continue;
}
Segment::TypeCondition(type_names, condition) => {
let next_selection_set_option =
current_selection_set
.items
.iter_mut()
.find_map(|item| match item {
SelectionItem::Field(_) => None,
SelectionItem::InlineFragment(f) => {
if type_names.contains(&f.type_condition)
&& fragment_condition_equal(condition, f)
{
Some(&mut f.selections)
} else {
None
}
}
SelectionItem::FragmentSpread(_) => None,
});
match next_selection_set_option {
Some(next_set) => {
current_selection_set = next_set;
}
None => {
return None;
}
}
}
Segment::Field(field_seg, args_hash, condition) => {
let next_selection_set_option =
current_selection_set
.items
.iter_mut()
.find_map(|item| match item {
SelectionItem::Field(field) => {
if field.selection_identifier() == field_seg.response_key()
&& field.arguments_hash() == *args_hash
&& field_condition_equal(condition, field)
{
Some(&mut field.selections)
} else {
None
}
}
SelectionItem::InlineFragment(..) => None,
SelectionItem::FragmentSpread(_) => None,
});
match next_selection_set_option {
Some(next_set) => {
current_selection_set = next_set;
}
None => {
return None;
}
}
}
}
}
Some(current_selection_set)
}
pub fn field_condition_equal(cond: &Option<Condition>, field: &FieldSelection) -> bool {
match cond {
Some(cond) => match cond {
Condition::Include(var_name) => {
field.include_if.as_ref().is_some_and(|v| v == var_name) && field.skip_if.is_none()
}
Condition::Skip(var_name) => {
field.skip_if.as_ref().is_some_and(|v| v == var_name) && field.include_if.is_none()
}
Condition::SkipAndInclude { skip, include } => {
field.skip_if.as_ref().is_some_and(|v| v == skip)
&& field.include_if.as_ref().is_some_and(|v| v == include)
}
},
None => field.include_if.is_none() && field.skip_if.is_none(),
}
}
fn fragment_condition_equal(cond: &Option<Condition>, fragment: &InlineFragmentSelection) -> bool {
match cond {
Some(cond) => match cond {
Condition::Include(var_name) => {
fragment.include_if.as_ref().is_some_and(|v| v == var_name)
&& fragment.skip_if.is_none()
}
Condition::Skip(var_name) => {
fragment.skip_if.as_ref().is_some_and(|v| v == var_name)
&& fragment.include_if.is_none()
}
Condition::SkipAndInclude { skip, include } => {
fragment.skip_if.as_ref().is_some_and(|v| v == skip)
&& fragment.include_if.as_ref().is_some_and(|v| v == include)
}
},
None => fragment.include_if.is_none() && fragment.skip_if.is_none(),
}
}
pub fn find_arguments_conflicts(
source: &SelectionSet,
other: &SelectionSet,
) -> Vec<(usize, usize)> {
other
.items
.iter()
.enumerate()
.filter_map(|(index, other_selection)| {
if let SelectionItem::Field(other_field) = other_selection {
let other_identifier = other_field.selection_identifier();
let other_args_hash = other_field.arguments_hash();
let existing_in_self =
source
.items
.iter()
.enumerate()
.find_map(|(self_index, self_selection)| {
if let SelectionItem::Field(self_field) = self_selection {
if self_field.selection_identifier() == other_identifier
&& self_field.arguments_hash() != other_args_hash
{
return Some(self_index);
}
}
None
});
if let Some(existing_index) = existing_in_self {
return Some((existing_index, index));
}
return None;
}
None
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::query_planner::ast::value::Value;
use super::*;
#[test]
fn print_alias_selection_set() {
let selection_set = SelectionSet {
items: vec![
SelectionItem::Field(FieldSelection {
name: "field1".to_string(),
selections: SelectionSet::default(),
alias: Some("f".to_string()),
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
}),
SelectionItem::Field(FieldSelection {
name: "field2".to_string(),
selections: SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "nested".to_string(),
selections: SelectionSet::default(),
alias: Some("n".to_string()),
arguments: Some(("a".to_string(), Value::Int(1)).into()),
skip_if: None,
include_if: None,
omit_from_response: false,
})],
},
alias: Some("f2".to_string()),
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
}),
],
};
insta::assert_snapshot!(
selection_set,
@"{f: field1 f2: field2{n: nested(a: 1)}}"
)
}
#[test]
fn print_simple_selection_set() {
let selection_set = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "field1".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
})],
};
insta::assert_snapshot!(
selection_set,
@"{field1}"
)
}
#[test]
fn selection_set_with_arguments() {
let selection_set = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "field1".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: Some(vec![("id".to_string(), Value::Int(1))].into()),
skip_if: None,
include_if: None,
omit_from_response: false,
})],
};
insta::assert_snapshot!(
selection_set,
@"{field1(id: 1)}"
)
}
#[test]
fn complex_selection_set() {
let selection_set = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "field1".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: Some(
vec![
("id".to_string(), Value::Int(1)),
("name".to_string(), Value::String("test".to_string())),
(
"list".to_string(),
Value::List(vec![Value::Int(1), Value::Int(2)]),
),
(
"obj".to_string(),
Value::Object(
vec![("key".to_string(), Value::String("value".to_string()))]
.into_iter()
.collect(),
),
),
]
.into(),
),
skip_if: None,
include_if: None,
omit_from_response: false,
})],
};
insta::assert_snapshot!(
selection_set,
@r#"{field1(id: 1, list: [1, 2], name: "test", obj: {key: "value"})}"#
)
}
#[test]
fn merge_selection_set_keeps_fields_with_different_conditions_separate() {
let mut target = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "reviews".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: None,
include_if: Some("first".to_string()),
omit_from_response: false,
})],
};
let source = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "reviews".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: None,
include_if: Some("second".to_string()),
omit_from_response: false,
})],
};
merge_selection_set(&mut target, &source, false);
assert_eq!(target.items.len(), 2);
}
#[test]
fn merge_selection_set_merges_fields_with_same_condition() {
let mut target = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "reviews".to_string(),
selections: SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "id".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
})],
},
alias: None,
arguments: None,
skip_if: None,
include_if: Some("cond".to_string()),
omit_from_response: false,
})],
};
let source = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "reviews".to_string(),
selections: SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "body".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: None,
include_if: None,
omit_from_response: false,
})],
},
alias: None,
arguments: None,
skip_if: None,
include_if: Some("cond".to_string()),
omit_from_response: false,
})],
};
merge_selection_set(&mut target, &source, false);
assert_eq!(target.items.len(), 1);
let SelectionItem::Field(field) = &target.items[0] else {
panic!("expected field selection");
};
assert_eq!(field.selections.items.len(), 2);
}
#[test]
fn merge_selection_set_keeps_inline_fragments_with_different_conditions_separate() {
let mut target = SelectionSet {
items: vec![SelectionItem::InlineFragment(InlineFragmentSelection {
type_condition: "User".to_string(),
selections: SelectionSet::default(),
skip_if: None,
include_if: Some("first".to_string()),
})],
};
let source = SelectionSet {
items: vec![SelectionItem::InlineFragment(InlineFragmentSelection {
type_condition: "User".to_string(),
selections: SelectionSet::default(),
skip_if: None,
include_if: Some("second".to_string()),
})],
};
merge_selection_set(&mut target, &source, false);
assert_eq!(target.items.len(), 2);
}
#[test]
fn merge_selection_set_prefers_visible_typename_over_skipped() {
let mut target = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection::new_skipped_typename())],
};
let source = SelectionSet {
items: vec![SelectionItem::Field(FieldSelection::new_typename())],
};
merge_selection_set(&mut target, &source, false);
let [SelectionItem::Field(field)] = target.items.as_slice() else {
panic!("expected exactly one __typename field");
};
assert_eq!(field.name, "__typename");
assert!(!field.omit_from_response);
}
#[test]
fn field_condition_skip_and_include() {
let skip_cond = Some(Condition::Skip("skip".to_string()));
let include_cond: Option<Condition> = Some(Condition::Include("include".to_string()));
let skip_and_include_cond = Some(Condition::SkipAndInclude {
skip: "skip".to_string(),
include: "include".to_string(),
});
let field = FieldSelection {
name: "name".to_string(),
selections: SelectionSet::default(),
alias: None,
arguments: None,
skip_if: Some("skip".to_string()),
include_if: Some("include".to_string()),
omit_from_response: false,
};
assert!(!field_condition_equal(&skip_cond, &field));
assert!(!field_condition_equal(&include_cond, &field));
assert!(field_condition_equal(&skip_and_include_cond, &field));
}
#[test]
fn fragment_condition_skip_and_include() {
let skip_cond = Some(Condition::Skip("skip".to_string()));
let include_cond: Option<Condition> = Some(Condition::Include("include".to_string()));
let skip_and_include_cond = Some(Condition::SkipAndInclude {
skip: "skip".to_string(),
include: "include".to_string(),
});
let fragment = InlineFragmentSelection {
type_condition: "Product".to_string(),
selections: SelectionSet::default(),
skip_if: Some("skip".to_string()),
include_if: Some("include".to_string()),
};
assert!(!fragment_condition_equal(&skip_cond, &fragment));
assert!(!fragment_condition_equal(&include_cond, &fragment));
assert!(fragment_condition_equal(&skip_and_include_cond, &fragment));
}
}