1- use std :: collections :: hash_map :: DefaultHasher ;
2- use std:: hash:: { Hash , Hasher } ;
1+ use rustc_hash :: { FxBuildHasher , FxHasher } ;
2+ use std:: hash:: { BuildHasher , Hash , Hasher } ;
33
44use crate :: ast:: arguments:: ArgumentsMap ;
55use crate :: ast:: operation:: { OperationDefinition , VariableDefinition } ;
@@ -14,7 +14,7 @@ pub trait ASTHash {
1414}
1515
1616pub fn ast_hash ( query : & OperationDefinition ) -> u64 {
17- let mut hasher = DefaultHasher :: new ( ) ;
17+ let mut hasher = FxHasher :: default ( ) ;
1818 query. ast_hash ( & mut hasher) ;
1919 hasher. finish ( )
2020}
@@ -111,32 +111,38 @@ impl ASTHash for &InlineFragmentSelection {
111111}
112112
113113impl ASTHash for ArgumentsMap {
114- fn ast_hash < H : Hasher > ( & self , hasher : & mut H ) {
115- // Order does not matter for hashing
116- // The order of arguments does not matter.
117- // To achieve order-insensitivity, we get all keys, sort them, and then
118- // hash them with their values in that order.
119- let mut keys: Vec < _ > = self . keys ( ) . collect ( ) ;
120- keys. sort_unstable ( ) ;
121- for key in keys {
122- key. hash ( hasher) ;
123- // We can unwrap here because we are iterating over existing keys
124- self . get_argument ( key) . unwrap ( ) . ast_hash ( hasher) ;
114+ fn ast_hash < H : Hasher > ( & self , state : & mut H ) {
115+ let mut combined_hash: u64 = 0 ;
116+ let build_hasher = FxBuildHasher ;
117+
118+ // To achieve an order-independent hash, we hash each key-value pair
119+ // individually and then combine their hashes using XOR (^).
120+ // Since XOR is commutative, the final hash is not affected by the iteration order.
121+ for ( key, value) in self . into_iter ( ) {
122+ let mut key_val_hasher = build_hasher. build_hasher ( ) ;
123+ key. hash ( & mut key_val_hasher) ;
124+ value. ast_hash ( & mut key_val_hasher) ;
125+ combined_hash ^= key_val_hasher. finish ( ) ;
125126 }
127+
128+ state. write_u64 ( combined_hash) ;
126129 }
127130}
128131
129132impl ASTHash for Vec < VariableDefinition > {
130133 fn ast_hash < H : Hasher > ( & self , hasher : & mut H ) {
131- // Order does not matter for hashing
132- // The order of variables does not matter.
133- // We do a similar thing as for ArgumentsMap,
134- // we sort the variables by their names.
135- let mut variables: Vec < _ > = self . iter ( ) . collect ( ) ;
136- variables. sort_by_key ( |f| & f. name ) ;
137- for var in variables {
138- var. ast_hash ( hasher) ;
134+ let mut combined_hash: u64 = 0 ;
135+ let build_hasher = FxBuildHasher ;
136+ // To achieve an order-independent hash, we hash each key-value pair
137+ // individually and then combine their hashes using XOR (^).
138+ // Since XOR is commutative, the final hash is not affected by the iteration order.
139+ for variable in self . iter ( ) {
140+ let mut local_hasher = build_hasher. build_hasher ( ) ;
141+ variable. ast_hash ( & mut local_hasher) ;
142+ combined_hash ^= local_hasher. finish ( ) ;
139143 }
144+
145+ hasher. write_u64 ( combined_hash) ;
140146 }
141147}
142148
@@ -196,3 +202,164 @@ impl ASTHash for Value {
196202 }
197203 }
198204}
205+
206+ #[ cfg( test) ]
207+ mod tests {
208+ use super :: * ;
209+ use crate :: ast:: arguments:: ArgumentsMap ;
210+ use crate :: ast:: operation:: { OperationDefinition , VariableDefinition } ;
211+ use crate :: ast:: selection_item:: SelectionItem ;
212+ use crate :: ast:: selection_set:: { FieldSelection , SelectionSet } ;
213+ use crate :: ast:: value:: Value ;
214+ use crate :: state:: supergraph_state:: { OperationKind , TypeNode } ;
215+ use std:: collections:: BTreeMap ;
216+
217+ fn create_test_operation ( ) -> OperationDefinition {
218+ let mut arguments = ArgumentsMap :: new ( ) ;
219+ arguments. add_argument ( "limit" . to_string ( ) , Value :: Int ( 10 ) ) ;
220+ arguments. add_argument ( "sort" . to_string ( ) , Value :: Enum ( "ASC" . to_string ( ) ) ) ;
221+
222+ let mut nested_object = BTreeMap :: new ( ) ;
223+ nested_object. insert (
224+ "nestedKey" . to_string ( ) ,
225+ Value :: String ( "nestedValue" . to_string ( ) ) ,
226+ ) ;
227+
228+ arguments. add_argument ( "obj" . to_string ( ) , Value :: Object ( nested_object) ) ;
229+
230+ let field_selection = FieldSelection {
231+ name : "users" . to_string ( ) ,
232+ alias : Some ( "all_users" . to_string ( ) ) ,
233+ selections : SelectionSet {
234+ items : vec ! [
235+ SelectionItem :: Field ( FieldSelection {
236+ name: "id" . to_string( ) ,
237+ alias: None ,
238+ selections: SelectionSet { items: vec![ ] } ,
239+ arguments: None ,
240+ include_if: None ,
241+ skip_if: None ,
242+ } ) ,
243+ SelectionItem :: Field ( FieldSelection {
244+ name: "name" . to_string( ) ,
245+ alias: None ,
246+ selections: SelectionSet { items: vec![ ] } ,
247+ arguments: None ,
248+ include_if: Some ( "includeName" . to_string( ) ) ,
249+ skip_if: None ,
250+ } ) ,
251+ ] ,
252+ } ,
253+ arguments : Some ( arguments) ,
254+ include_if : None ,
255+ skip_if : Some ( "skipUsers" . to_string ( ) ) ,
256+ } ;
257+
258+ let selection_set = SelectionSet {
259+ items : vec ! [ SelectionItem :: Field ( field_selection) ] ,
260+ } ;
261+
262+ let variable_definitions = vec ! [
263+ VariableDefinition {
264+ name: "skipUsers" . to_string( ) ,
265+ variable_type: TypeNode :: NonNull ( Box :: new( TypeNode :: Named ( "Boolean" . to_string( ) ) ) ) ,
266+ default_value: Some ( Value :: Boolean ( false ) ) ,
267+ } ,
268+ VariableDefinition {
269+ name: "includeName" . to_string( ) ,
270+ variable_type: TypeNode :: Named ( "Boolean" . to_string( ) ) ,
271+ default_value: None ,
272+ } ,
273+ ] ;
274+
275+ OperationDefinition {
276+ operation_kind : Some ( OperationKind :: Query ) ,
277+ selection_set,
278+ variable_definitions : Some ( variable_definitions) ,
279+ name : Some ( "TestQuery" . to_string ( ) ) ,
280+ }
281+ }
282+
283+ #[ test]
284+ fn test_ast_hash_is_deterministic ( ) {
285+ let operation = create_test_operation ( ) ;
286+
287+ let hash1 = ast_hash ( & operation) ;
288+ let hash2 = ast_hash ( & operation) ;
289+
290+ // Test that the hash is consistent within the same run
291+ assert_eq ! ( hash1, hash2, "AST hash should be consistent" ) ;
292+
293+ // Snapshot test: compare against a known, pre-calculated hash.
294+ // If the hashing logic changes, this value will need to be updated.
295+ let expected_hash = 8854078506550230644 ;
296+ assert_eq ! (
297+ hash1, expected_hash,
298+ "AST hash does not match the snapshot value. If this change is intentional, update the snapshot."
299+ ) ;
300+ }
301+
302+ #[ test]
303+ fn test_order_independent_hashing_for_arguments ( ) {
304+ let mut args1 = ArgumentsMap :: new ( ) ;
305+ args1. add_argument ( "a" . to_string ( ) , Value :: Int ( 1 ) ) ;
306+ args1. add_argument ( "b" . to_string ( ) , Value :: Int ( 2 ) ) ;
307+
308+ let mut args2 = ArgumentsMap :: new ( ) ;
309+ args2. add_argument ( "b" . to_string ( ) , Value :: Int ( 2 ) ) ;
310+ args2. add_argument ( "a" . to_string ( ) , Value :: Int ( 1 ) ) ;
311+
312+ let mut hasher1 = FxHasher :: default ( ) ;
313+ args1. ast_hash ( & mut hasher1) ;
314+
315+ let mut hasher2 = FxHasher :: default ( ) ;
316+ args2. ast_hash ( & mut hasher2) ;
317+
318+ assert_eq ! (
319+ hasher1. finish( ) ,
320+ hasher2. finish( ) ,
321+ "ArgumentsMap hashing should be order-independent"
322+ ) ;
323+ }
324+
325+ #[ test]
326+ fn test_order_independent_hashing_for_variables ( ) {
327+ let vars1 = vec ! [
328+ VariableDefinition {
329+ name: "varA" . to_string( ) ,
330+ variable_type: TypeNode :: Named ( "String" . to_string( ) ) ,
331+ default_value: None ,
332+ } ,
333+ VariableDefinition {
334+ name: "varB" . to_string( ) ,
335+ variable_type: TypeNode :: Named ( "Int" . to_string( ) ) ,
336+ default_value: Some ( Value :: Int ( 0 ) ) ,
337+ } ,
338+ ] ;
339+
340+ let vars2 = vec ! [
341+ VariableDefinition {
342+ name: "varB" . to_string( ) ,
343+ variable_type: TypeNode :: Named ( "Int" . to_string( ) ) ,
344+ default_value: Some ( Value :: Int ( 0 ) ) ,
345+ } ,
346+ VariableDefinition {
347+ name: "varA" . to_string( ) ,
348+ variable_type: TypeNode :: Named ( "String" . to_string( ) ) ,
349+ default_value: None ,
350+ } ,
351+ ] ;
352+
353+ let mut hasher1 = FxHasher :: default ( ) ;
354+ vars1. ast_hash ( & mut hasher1) ;
355+
356+ let mut hasher2 = FxHasher :: default ( ) ;
357+ vars2. ast_hash ( & mut hasher2) ;
358+
359+ assert_eq ! (
360+ hasher1. finish( ) ,
361+ hasher2. finish( ) ,
362+ "VariableDefinition vector hashing should be order-independent"
363+ ) ;
364+ }
365+ }
0 commit comments