Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions bluejay-core/src/argument.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::{AsIter, Value};
use std::collections::HashMap;

pub trait Argument<const CONST: bool> {
type Value: Value<CONST>;
Expand All @@ -18,17 +17,26 @@ pub trait Arguments<const CONST: bool>: AsIter<Item = Self::Argument> {
type Argument: Argument<CONST>;

fn equivalent(optional_self: Option<&Self>, optional_other: Option<&Self>) -> bool {
let lhs: HashMap<&str, _> = optional_self
.map(|args| {
HashMap::from_iter(args.iter().map(|arg| (arg.name(), arg.value().as_ref())))
})
.unwrap_or_default();
let rhs: HashMap<&str, _> = optional_other
.map(|args| {
HashMap::from_iter(args.iter().map(|arg| (arg.name(), arg.value().as_ref())))
})
.unwrap_or_default();
lhs == rhs
match (optional_self, optional_other) {
(None, None) => true,
(None, Some(other)) => other.is_empty(),
(Some(s), None) => s.is_empty(),
(Some(s), Some(other)) => {
// For small argument lists, use O(n*m) comparison which avoids HashMap allocation
let s_count = s.len();
let o_count = other.len();
if s_count != o_count {
return false;
}
// Every arg in self must have a matching arg in other (same name, same value)
s.iter().all(|s_arg| {
other.iter().any(|o_arg| {
s_arg.name() == o_arg.name()
&& s_arg.value().as_ref() == o_arg.value().as_ref()
})
})
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bluejay_core::{
definition::{prelude::*, BaseOutputTypeReference, SchemaDefinition},
executable::{ExecutableDocument, Field, Selection, SelectionReference},
executable::{ExecutableDocument, Field},
};
use bluejay_validator::executable::{
document::{Analyzer, Path, Visitor},
Expand All @@ -11,6 +11,7 @@ use std::collections::HashSet;
pub(crate) struct PathsWithCustomScalarType<'a, S: SchemaDefinition> {
schema_definition: &'a S,
paths: HashSet<Vec<String>>,
field_stack: Vec<&'a str>,
}

impl<'a, E: ExecutableDocument, S: SchemaDefinition> Visitor<'a, E, S>
Expand All @@ -20,33 +21,40 @@ impl<'a, E: ExecutableDocument, S: SchemaDefinition> Visitor<'a, E, S>
Self {
schema_definition,
paths: HashSet::new(),
field_stack: Vec::new(),
}
}

fn visit_field(
&mut self,
_field: &'a <E as ExecutableDocument>::Field,
field: &'a <E as ExecutableDocument>::Field,
field_definition: &'a <S as SchemaDefinition>::FieldDefinition,
path: &Path<'a, E>,
) {
self.field_stack.push(field.response_name());

if matches!(
field_definition.r#type().base(self.schema_definition),
BaseOutputTypeReference::CustomScalar(_)
) {
if let Some(root_name) = path.root().name() {
self.paths.insert(
std::iter::once(root_name.to_string())
.chain(path.members().iter().filter_map(
|selection| match selection.as_ref() {
SelectionReference::Field(f) => Some(f.response_name().to_string()),
_ => None,
},
))
std::iter::once(root_name)
.chain(self.field_stack.iter().copied())
.map(String::from)
.collect(),
);
}
}
}

fn leave_field(
&mut self,
_field: &'a <E as ExecutableDocument>::Field,
_field_definition: &'a <S as SchemaDefinition>::FieldDefinition,
) {
self.field_stack.pop();
}
}

impl<'a, E: ExecutableDocument, S: SchemaDefinition> Analyzer<'a, E, S>
Expand Down
4 changes: 4 additions & 0 deletions bluejay-validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,9 @@ serde_json = ["dep:serde_json", "bluejay-core/serde_json"]
name = "field_selection_merging"
harness = false

[[bench]]
name = "validate"
harness = false

[lints]
workspace = true
186 changes: 186 additions & 0 deletions bluejay-validator/benches/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use bluejay_parser::ast::{
definition::{DefinitionDocument, SchemaDefinition},
executable::ExecutableDocument,
Parse,
};
use bluejay_validator::executable::{document::BuiltinRulesValidator, Cache};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use std::sync::LazyLock;

const SCHEMA: &str = include_str!("../tests/test_data/executable/schema.graphql");

// A realistic query with fragments, variables, inline fragments, and nested fields
const QUERY_SIMPLE: &str = r#"
query GetDog($command: DogCommand!, $atOtherHomes: Boolean) {
dog {
name
nickname
barkVolume
doesKnowCommand(dogCommand: $command)
isHouseTrained(atOtherHomes: $atOtherHomes)
owner {
name
}
}
}
"#;

const QUERY_FRAGMENTS: &str = r#"
query GetPetWithFragments($command: DogCommand!) {
dog {
...dogFields
owner {
...humanFields
}
}
pet {
...petFields
}
}

fragment dogFields on Dog {
name
nickname
barkVolume
doesKnowCommand(dogCommand: $command)
owner {
...humanFields
}
}

fragment humanFields on Human {
name
}

fragment petFields on Pet {
name
... on Dog {
barkVolume
...dogFields
}
... on Cat {
meowVolume
}
}
"#;

const QUERY_COMPLEX: &str = r#"
query Complex($command: DogCommand!, $atOtherHomes: Boolean) {
dog {
...f1
...f2
...f3
}
pet {
...petFragment
}
}

fragment f1 on Dog {
name
nickname
barkVolume
doesKnowCommand(dogCommand: $command)
isHouseTrained(atOtherHomes: $atOtherHomes)
owner { ...humanFrag }
}

fragment f2 on Dog {
name
nickname
barkVolume
doesKnowCommand(dogCommand: $command)
isHouseTrained(atOtherHomes: $atOtherHomes)
owner { ...humanFrag }
}

fragment f3 on Dog {
name
nickname
barkVolume
doesKnowCommand(dogCommand: $command)
isHouseTrained(atOtherHomes: $atOtherHomes)
owner { ...humanFrag }
}

fragment humanFrag on Human {
name
}

fragment petFragment on Pet {
name
... on Dog {
barkVolume
nickname
...f1
}
... on Cat {
meowVolume
nickname
}
}
"#;

static DEFINITION_DOCUMENT: LazyLock<DefinitionDocument<'static>> = LazyLock::new(|| {
DefinitionDocument::parse(SCHEMA)
.result
.expect("Schema had parse errors")
});
static SCHEMA_DEFINITION: LazyLock<SchemaDefinition<'static>> =
LazyLock::new(|| SchemaDefinition::try_from(&*DEFINITION_DOCUMENT).expect("Schema had errors"));

struct QueryCase {
name: &'static str,
source: &'static str,
}

static CASES: &[QueryCase] = &[
QueryCase {
name: "simple",
source: QUERY_SIMPLE,
},
QueryCase {
name: "fragments",
source: QUERY_FRAGMENTS,
},
QueryCase {
name: "complex",
source: QUERY_COMPLEX,
},
];

fn bench_validate(c: &mut Criterion) {
let mut group = c.benchmark_group("validate");
for case in CASES {
let doc = ExecutableDocument::parse(case.source)
.result
.expect("Document had parse errors");
group.bench_with_input(BenchmarkId::from_parameter(case.name), &doc, |b, doc| {
b.iter(|| {
let cache = Cache::new(doc, &*SCHEMA_DEFINITION);
let errors: Vec<_> =
BuiltinRulesValidator::validate(doc, &*SCHEMA_DEFINITION, &cache).collect();
assert!(errors.is_empty());
});
});
}
group.finish();
}

fn bench_cache_construction(c: &mut Criterion) {
let mut group = c.benchmark_group("cache_construction");
for case in CASES {
let doc = ExecutableDocument::parse(case.source)
.result
.expect("Document had parse errors");
group.bench_with_input(BenchmarkId::from_parameter(case.name), &doc, |b, doc| {
b.iter(|| {
let _cache = Cache::new(doc, &*SCHEMA_DEFINITION);
});
});
}
group.finish();
}

criterion_group!(benches, bench_validate, bench_cache_construction);
criterion_main!(benches);
15 changes: 8 additions & 7 deletions bluejay-validator/src/executable/document/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,26 +122,25 @@ impl<'a, E: ExecutableDocument, S: SchemaDefinition, V: Visitor<'a, E, S>>
) {
self.visitor.visit_selection_set(selection_set, scoped_type);

selection_set.iter().for_each(|selection| {
let nested_path = path.with_selection(selection);
match selection.as_ref() {
selection_set
.iter()
.for_each(|selection| match selection.as_ref() {
SelectionReference::Field(f) => {
let field_definition = scoped_type
.fields_definition()
.and_then(|fields_definition| fields_definition.get(f.name()));

if let Some(field_definition) = field_definition {
self.visit_field(f, field_definition, &nested_path);
self.visit_field(f, field_definition, path);
}
}
SelectionReference::InlineFragment(i) => {
self.visit_inline_fragment(i, scoped_type, &nested_path)
self.visit_inline_fragment(i, scoped_type, path)
}
SelectionReference::FragmentSpread(fs) => {
self.visit_fragment_spread(fs, scoped_type, path)
}
}
})
})
}

fn visit_field(
Expand Down Expand Up @@ -170,6 +169,8 @@ impl<'a, E: ExecutableDocument, S: SchemaDefinition, V: Visitor<'a, E, S>>
self.visit_selection_set(selection_set, nested_type, path);
}
}

self.visitor.leave_field(field, field_definition);
}

fn visit_variable_directives(
Expand Down
23 changes: 4 additions & 19 deletions bluejay-validator/src/executable/document/path.rs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just delete this then since it doesn't do what it's name suggests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm multiple rules do use PathRoot though. Ideally Path is removed and simplified down to just PathRoot? I'd rather defer that cleanup since it was cause a lot of (trivial) changes

Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,24 @@ use std::hash::{Hash, Hasher};

pub struct Path<'a, E: ExecutableDocument> {
root: PathRoot<'a, E>,
members: Vec<&'a E::Selection>,
}

impl<E: ExecutableDocument> Clone for Path<'_, E> {
fn clone(&self) -> Self {
Self {
root: self.root,
members: self.members.clone(),
}
*self
}
}

impl<E: ExecutableDocument> Copy for Path<'_, E> {}

impl<'a, E: ExecutableDocument> Path<'a, E> {
pub fn new(root: PathRoot<'a, E>) -> Self {
Self {
root,
members: Vec::new(),
}
Self { root }
}

pub fn root(&self) -> &PathRoot<'a, E> {
&self.root
}

pub fn with_selection(&self, selection: &'a E::Selection) -> Self {
let mut clone = self.clone();
clone.members.push(selection);
clone
}

pub fn members(&self) -> &[&'a E::Selection] {
&self.members
}
}

pub enum PathRoot<'a, E: ExecutableDocument> {
Expand Down
Loading
Loading