code: def exec(self, *args, **kwargs) -> Iterator[Batch]:
      build_table = self.children[0]
      probe_table = self.children[1]
      hash_keys = [key.col_alias for key in self.probe_keys]
      for build_batch in build_table.exec():
          for probe_batch in probe_table.exec():
              probe_batch.reassign_indices_to_hash(hash_keys)
              join_batch = Batch.join(probe_batch, build_batch)
              join_batch.reset_index()
              join_batch = apply_predicate(join_batch, self.predicate)
              join_batch = apply_project(join_batch, self.join_project)
              yield join_batch
  
      # instrument required stats
      if self.predicate or self.join_project:
          catalog = self.catalog()
          instrument_function_expression_cost(self.predicate, catalog)
          instrument_function_expression_cost(
              self.join_project, catalog
          )
  
file_path: evadb/evadb/executor/hash_join_executor.py
imports: from typing import Iterator
  from evadb.database import EvaDBDatabase
  from evadb.executor.abstract_executor import AbstractExecutor
  from evadb.executor.executor_utils import apply_predicate, apply_project, instrument_function_expression_cost
  from evadb.models.storage.batch import Batch
  from evadb.plan_nodes.hash_join_probe_plan import HashJoinProbePlan
label_desc: Use image node_id 2 for calling the HashJoinExecutor obj's underlying member method code with example usage: obj.exec() without return types