Skip to content

Commit c65d602

Browse files
committed
graphql: Add a way to hash just the 'shape' of a GraphQL query
1 parent 6cdd95f commit c65d602

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

graphql/src/query/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub mod ast;
1414
/// Extension traits
1515
pub mod ext;
1616

17+
/// Hashing the 'shape' of a query
18+
pub mod shape_hash;
19+
1720
/// Options available for query execution.
1821
pub struct QueryExecutionOptions<R>
1922
where

graphql/src/query/shape_hash.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//! Calculate a hash for a GraphQL query that reflects the shape of
2+
//! the query. The shape hash will be the same for two instancs of a query
3+
//! that are deemed identical except for unimportant details. Those details
4+
//! are any values used with filters, and any differences in the query
5+
//! name or response keys
6+
7+
use graphql_parser::query as q;
8+
use graphql_parser::schema as s;
9+
use std::collections::hash_map::DefaultHasher;
10+
use std::hash::{Hash, Hasher};
11+
12+
type ShapeHasher = DefaultHasher;
13+
14+
pub trait ShapeHash {
15+
fn shape_hash(&self, hasher: &mut ShapeHasher);
16+
}
17+
18+
pub fn shape_hash(query: &q::Document) -> u64 {
19+
let mut hasher = DefaultHasher::new();
20+
query.shape_hash(&mut hasher);
21+
hasher.finish()
22+
}
23+
24+
// In all ShapeHash implementations, we never include anything to do with
25+
// the position of the element in the query, i.e., fields that involve
26+
// `Pos`
27+
28+
impl ShapeHash for q::Document {
29+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
30+
for defn in &self.definitions {
31+
use q::Definition::*;
32+
match defn {
33+
Operation(op) => op.shape_hash(hasher),
34+
Fragment(frag) => frag.shape_hash(hasher),
35+
}
36+
}
37+
}
38+
}
39+
40+
impl ShapeHash for q::OperationDefinition {
41+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
42+
use q::OperationDefinition::*;
43+
// We want `[query|subscription|mutation] things { BODY }` to hash
44+
// to the same thing as just `things { BODY }`
45+
match self {
46+
SelectionSet(set) => set.shape_hash(hasher),
47+
Query(query) => query.selection_set.shape_hash(hasher),
48+
Mutation(mutation) => mutation.selection_set.shape_hash(hasher),
49+
Subscription(subscription) => subscription.selection_set.shape_hash(hasher),
50+
}
51+
}
52+
}
53+
54+
impl ShapeHash for q::FragmentDefinition {
55+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
56+
// Omit directives
57+
self.name.hash(hasher);
58+
self.type_condition.shape_hash(hasher);
59+
self.selection_set.shape_hash(hasher);
60+
}
61+
}
62+
63+
impl ShapeHash for q::SelectionSet {
64+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
65+
for item in &self.items {
66+
item.shape_hash(hasher);
67+
}
68+
}
69+
}
70+
71+
impl ShapeHash for q::Selection {
72+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
73+
use q::Selection::*;
74+
match self {
75+
Field(field) => field.shape_hash(hasher),
76+
FragmentSpread(spread) => spread.shape_hash(hasher),
77+
InlineFragment(frag) => frag.shape_hash(hasher),
78+
}
79+
}
80+
}
81+
82+
impl ShapeHash for q::Field {
83+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
84+
// Omit alias, directives
85+
self.name.hash(hasher);
86+
self.selection_set.shape_hash(hasher);
87+
for (name, value) in &self.arguments {
88+
name.hash(hasher);
89+
value.shape_hash(hasher);
90+
}
91+
}
92+
}
93+
94+
impl ShapeHash for s::Value {
95+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
96+
use s::Value::*;
97+
98+
match self {
99+
Variable(_) | Int(_) | Float(_) | String(_) | Boolean(_) | Null | Enum(_) => {
100+
/* ignore */
101+
}
102+
List(values) => {
103+
for value in values {
104+
value.shape_hash(hasher);
105+
}
106+
}
107+
Object(map) => {
108+
for (name, value) in map {
109+
name.hash(hasher);
110+
value.shape_hash(hasher);
111+
}
112+
}
113+
}
114+
}
115+
}
116+
117+
impl ShapeHash for q::FragmentSpread {
118+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
119+
// Omit directives
120+
self.fragment_name.hash(hasher)
121+
}
122+
}
123+
124+
impl ShapeHash for q::InlineFragment {
125+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
126+
// Omit directives
127+
self.type_condition.shape_hash(hasher);
128+
self.selection_set.shape_hash(hasher);
129+
}
130+
}
131+
132+
impl<T: ShapeHash> ShapeHash for Option<T> {
133+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
134+
match self {
135+
None => false.hash(hasher),
136+
Some(t) => {
137+
Some(true).hash(hasher);
138+
t.shape_hash(hasher);
139+
}
140+
}
141+
}
142+
}
143+
144+
impl ShapeHash for q::TypeCondition {
145+
fn shape_hash(&self, hasher: &mut ShapeHasher) {
146+
match self {
147+
q::TypeCondition::On(value) => value.hash(hasher),
148+
}
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use super::*;
155+
use graphql_parser::parse_query;
156+
157+
#[test]
158+
fn identical_and_different() {
159+
const Q1: &str = "query things($stuff: Int) { things(where: { stuff_gt: $stuff }) { id } }";
160+
const Q2: &str = "{ things(where: { stuff_gt: 42 }) { id } }";
161+
const Q3: &str = "{ things(where: { stuff_lte: 42 }) { id } }";
162+
const Q4: &str = "{ things(where: { stuff_gt: 42 }) { id name } }";
163+
let q1 = parse_query(Q1).expect("q1 is syntactically valid");
164+
let q2 = parse_query(Q2).expect("q2 is syntactically valid");
165+
let q3 = parse_query(Q3).expect("q3 is syntactically valid");
166+
let q4 = parse_query(Q4).expect("q4 is syntactically valid");
167+
168+
assert_eq!(shape_hash(&q1), shape_hash(&q2));
169+
assert_ne!(shape_hash(&q1), shape_hash(&q3));
170+
assert_ne!(shape_hash(&q2), shape_hash(&q4));
171+
}
172+
}

0 commit comments

Comments
 (0)