Skip to content

Commit 7e1c0f6

Browse files
Make AST hash order-independent (opt-in) (#275)
No sorting needed in minfication, as ast_hash used in minification algo uses order-independent mode. It should speed up the minification phase. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 5545276 commit 7e1c0f6

22 files changed

+272
-289
lines changed

lib/query-planner/src/ast/hash.rs

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@ use crate::ast::selection_set::{FieldSelection, InlineFragmentSelection, Selecti
88
use crate::ast::value::Value;
99
use crate::state::supergraph_state::{self, OperationKind, TypeNode};
1010

11-
/// Order-dependent hashing
11+
/// A trait for hashing AST nodes, with support for both order-dependent and order-independent hashing.
1212
pub trait ASTHash {
13-
fn ast_hash<H: Hasher>(&self, hasher: &mut H);
13+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H);
1414
}
1515

1616
pub fn ast_hash(query: &OperationDefinition) -> u64 {
1717
let mut hasher = FxHasher::default();
18-
query.ast_hash(&mut hasher);
18+
query.ast_hash::<_, false>(&mut hasher);
1919
hasher.finish()
2020
}
2121
// In all ShapeHash implementations, we never include anything to do with
2222
// the position of the element in the query, i.e., fields that involve
2323
// `Pos`
2424

2525
impl ASTHash for &OperationKind {
26-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
26+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
2727
match self {
2828
OperationKind::Query => "Query".hash(hasher),
2929
OperationKind::Mutation => "Mutation".hash(hasher),
@@ -33,55 +33,72 @@ impl ASTHash for &OperationKind {
3333
}
3434

3535
impl ASTHash for OperationDefinition {
36-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
36+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
3737
self.operation_kind
3838
.as_ref()
3939
.or(Some(&supergraph_state::OperationKind::Query))
40-
.ast_hash(hasher);
40+
.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
4141

42-
self.selection_set.ast_hash(hasher);
43-
self.variable_definitions.ast_hash(hasher);
42+
self.selection_set.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
43+
self.variable_definitions
44+
.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
4445
}
4546
}
4647

4748
impl<T: ASTHash> ASTHash for Option<T> {
48-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
49+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
4950
match self {
5051
None => false.hash(hasher),
5152
Some(t) => {
5253
Some(true).hash(hasher);
53-
t.ast_hash(hasher);
54+
t.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
5455
}
5556
}
5657
}
5758
}
5859

5960
impl ASTHash for SelectionSet {
60-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
61-
for item in &self.items {
62-
item.ast_hash(hasher);
61+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
62+
if ORDER_INDEPENDENT {
63+
let mut combined_hash: u64 = 0;
64+
let build_hasher = FxBuildHasher;
65+
66+
// To achieve an order-independent hash, we hash each key-value pair
67+
// individually and then combine their hashes using XOR (^).
68+
// Since XOR is commutative, the final hash is not affected by the iteration order.
69+
for item in &self.items {
70+
let mut key_val_hasher = build_hasher.build_hasher();
71+
item.ast_hash::<_, ORDER_INDEPENDENT>(&mut key_val_hasher);
72+
combined_hash ^= key_val_hasher.finish();
73+
}
74+
75+
hasher.write_u64(combined_hash);
76+
} else {
77+
for item in &self.items {
78+
item.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
79+
}
6380
}
6481
}
6582
}
6683

6784
impl ASTHash for SelectionItem {
68-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
85+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
6986
match self {
70-
SelectionItem::Field(field) => field.ast_hash(hasher),
71-
SelectionItem::InlineFragment(frag) => frag.ast_hash(hasher),
87+
SelectionItem::Field(field) => field.ast_hash::<_, ORDER_INDEPENDENT>(hasher),
88+
SelectionItem::InlineFragment(frag) => frag.ast_hash::<_, ORDER_INDEPENDENT>(hasher),
7289
SelectionItem::FragmentSpread(name) => name.hash(hasher),
7390
}
7491
}
7592
}
7693

7794
impl ASTHash for &FieldSelection {
78-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
95+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
7996
self.name.hash(hasher);
8097
self.alias.hash(hasher);
81-
self.selections.ast_hash(hasher);
98+
self.selections.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
8299

83100
if let Some(args) = &self.arguments {
84-
args.ast_hash(hasher);
101+
args.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
85102
}
86103

87104
if let Some(var_name) = self.include_if.as_ref() {
@@ -96,9 +113,9 @@ impl ASTHash for &FieldSelection {
96113
}
97114

98115
impl ASTHash for &InlineFragmentSelection {
99-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
116+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
100117
self.type_condition.hash(hasher);
101-
self.selections.ast_hash(hasher);
118+
self.selections.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
102119
if let Some(var_name) = self.include_if.as_ref() {
103120
"@include".hash(hasher);
104121
var_name.hash(hasher);
@@ -111,7 +128,7 @@ impl ASTHash for &InlineFragmentSelection {
111128
}
112129

113130
impl ASTHash for ArgumentsMap {
114-
fn ast_hash<H: Hasher>(&self, state: &mut H) {
131+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, state: &mut H) {
115132
let mut combined_hash: u64 = 0;
116133
let build_hasher = FxBuildHasher;
117134

@@ -121,7 +138,7 @@ impl ASTHash for ArgumentsMap {
121138
for (key, value) in self.into_iter() {
122139
let mut key_val_hasher = build_hasher.build_hasher();
123140
key.hash(&mut key_val_hasher);
124-
value.ast_hash(&mut key_val_hasher);
141+
value.ast_hash::<_, ORDER_INDEPENDENT>(&mut key_val_hasher);
125142
combined_hash ^= key_val_hasher.finish();
126143
}
127144

@@ -130,15 +147,15 @@ impl ASTHash for ArgumentsMap {
130147
}
131148

132149
impl ASTHash for Vec<VariableDefinition> {
133-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
150+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
134151
let mut combined_hash: u64 = 0;
135152
let build_hasher = FxBuildHasher;
136153
// To achieve an order-independent hash, we hash each key-value pair
137154
// individually and then combine their hashes using XOR (^).
138155
// Since XOR is commutative, the final hash is not affected by the iteration order.
139156
for variable in self.iter() {
140157
let mut local_hasher = build_hasher.build_hasher();
141-
variable.ast_hash(&mut local_hasher);
158+
variable.ast_hash::<_, ORDER_INDEPENDENT>(&mut local_hasher);
142159
combined_hash ^= local_hasher.finish();
143160
}
144161

@@ -147,41 +164,41 @@ impl ASTHash for Vec<VariableDefinition> {
147164
}
148165

149166
impl ASTHash for VariableDefinition {
150-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
167+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
151168
self.name.hash(hasher);
152-
self.variable_type.ast_hash(hasher);
153-
self.default_value.ast_hash(hasher);
169+
self.variable_type.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
170+
self.default_value.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
154171
}
155172
}
156173

157174
impl ASTHash for TypeNode {
158-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
175+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
159176
match self {
160177
TypeNode::Named(name) => name.hash(hasher),
161178
TypeNode::List(inner) => {
162179
"list".hash(hasher);
163-
inner.ast_hash(hasher);
180+
inner.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
164181
}
165182
TypeNode::NonNull(inner) => {
166183
"non_null".hash(hasher);
167-
inner.ast_hash(hasher);
184+
inner.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
168185
}
169186
}
170187
}
171188
}
172189

173190
impl ASTHash for Value {
174-
fn ast_hash<H: Hasher>(&self, hasher: &mut H) {
191+
fn ast_hash<H: Hasher, const ORDER_INDEPENDENT: bool>(&self, hasher: &mut H) {
175192
match self {
176193
Value::List(values) => {
177194
for value in values {
178-
value.ast_hash(hasher);
195+
value.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
179196
}
180197
}
181198
Value::Object(map) => {
182199
for (name, value) in map {
183200
name.hash(hasher);
184-
value.ast_hash(hasher);
201+
value.ast_hash::<_, ORDER_INDEPENDENT>(hasher);
185202
}
186203
}
187204
Value::Null => {
@@ -310,10 +327,10 @@ mod tests {
310327
args2.add_argument("a".to_string(), Value::Int(1));
311328

312329
let mut hasher1 = FxHasher::default();
313-
args1.ast_hash(&mut hasher1);
330+
args1.ast_hash::<_, true>(&mut hasher1);
314331

315332
let mut hasher2 = FxHasher::default();
316-
args2.ast_hash(&mut hasher2);
333+
args2.ast_hash::<_, true>(&mut hasher2);
317334

318335
assert_eq!(
319336
hasher1.finish(),
@@ -351,10 +368,10 @@ mod tests {
351368
];
352369

353370
let mut hasher1 = FxHasher::default();
354-
vars1.ast_hash(&mut hasher1);
371+
vars1.ast_hash::<_, true>(&mut hasher1);
355372

356373
let mut hasher2 = FxHasher::default();
357-
vars2.ast_hash(&mut hasher2);
374+
vars2.ast_hash::<_, true>(&mut hasher2);
358375

359376
assert_eq!(
360377
hasher1.finish(),

lib/query-planner/src/ast/minification/mod.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
use crate::ast::document::Document;
2-
use crate::ast::minification::sort::sort_operation;
32
use crate::ast::minification::stats::Stats;
43
use crate::ast::minification::transform::transform_operation;
54
use crate::ast::{minification::error::MinificationError, operation::OperationDefinition};
65
use crate::state::supergraph_state::{OperationKind, SupergraphState};
76

87
pub mod error;
98
mod selection_id;
10-
mod sort;
119
mod stats;
1210
mod transform;
1311

1412
pub fn minify_operation(
1513
operation: OperationDefinition,
1614
supergraph: &SupergraphState,
1715
) -> Result<Document, MinificationError> {
18-
let operation = sort_operation(operation);
1916
let root_type_name = get_root_type_name(&operation, supergraph)?.to_string();
2017
let stats = Stats::from_operation(&operation.selection_set, supergraph, &root_type_name)?;
2118
transform_operation(supergraph, stats, &root_type_name, operation)
@@ -265,24 +262,24 @@ mod tests {
265262
@r"
266263
query($id: ID!) {
267264
product(id: $id) {
265+
id
266+
name
268267
distributor {
269268
...a
270269
}
271-
id
272-
name
273270
... on Book {
274271
relatedProducts {
272+
id
273+
name
275274
distributor {
276275
...a
277276
}
278-
id
279-
name
280277
... on Book {
281278
relatedProducts {
279+
id
282280
distributor {
283281
...c
284282
}
285-
id
286283
}
287284
}
288285
}
@@ -298,22 +295,22 @@ mod tests {
298295
fragment a on BusinessEntity {
299296
id
300297
name
301-
... on Manufacturer {
302-
...b
303-
}
304298
... on Supplier {
305-
__typename
306299
licenseNumber
300+
__typename
307301
}
308302
... on Vendor {
309-
__typename
310303
preferred
304+
__typename
305+
}
306+
... on Manufacturer {
307+
...b
311308
}
312309
}
313310
314311
fragment b on Manufacturer {
315-
__typename
316312
country
313+
__typename
317314
}
318315
319316
fragment c on BusinessEntity {

lib/query-planner/src/ast/minification/selection_id.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ pub type SelectionId = u64;
99
pub fn generate_selection_id(type_name: &str, selection_set: &SelectionSet) -> SelectionId {
1010
let mut hasher = DefaultHasher::new();
1111
type_name.hash(&mut hasher);
12-
selection_set.ast_hash(&mut hasher);
12+
selection_set.ast_hash::<_, true>(&mut hasher);
1313
hasher.finish()
1414
}

lib/query-planner/src/ast/minification/sort.rs

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)