docs: def ReadPathInfosHistories(self,
                               client_id,
                               path_type,
                               components_list,
                               cursor=None):
      """Reads a collection of hash and stat entries for given paths."""
      # MySQL does not handle well empty `IN` clauses so we guard against that.
      if not components_list:
        return {}
  
      path_infos = {components: [] for components in components_list}
  
      path_id_components = {}
      for components in components_list:
        path_id = rdf_objects.PathID.FromComponents(components)
        path_id_components[path_id] = components
  
      # MySQL does not support full outer joins, so we emulate them with a union.
      query = """
      SELECT s.path_id, s.stat_entry, UNIX_TIMESTAMP(s.timestamp),
             h.path_id, h.hash_entry, UNIX_TIMESTAMP(h.timestamp)
        FROM client_path_stat_entries AS s
   LEFT JOIN client_path_hash_entries AS h
          ON s.client_id = h.client_id
         AND s.path_type = h.path_type
         AND s.path_id = h.path_id
         AND s.timestamp = h.timestamp
       WHERE s.client_id = %(client_id)s
         AND s.path_type = %(path_type)s
         AND s.path_id IN %(path_ids)s
       UNION
      SELECT s.path_id, s.stat_entry, UNIX_TIMESTAMP(s.timestamp),
             h.path_id, h.hash_entry, UNIX_TIMESTAMP(h.timestamp)
        FROM client_path_hash_entries AS h
   LEFT JOIN client_path_stat_entries AS s
          ON h.client_id = s.client_id
         AND h.path_type = s.path_type
         AND h.path_id = s.path_id
         AND h.timestamp = s.timestamp
       WHERE h.client_id = %(client_id)s
         AND h.path_type = %(path_type)s
         AND h.path_id IN %(path_ids)s
      """
  
      params = {
          "client_id": db_utils.ClientIDToInt(client_id),
          "path_type": int(path_type),
          "path_ids": [path_id.AsBytes() for path_id in path_id_components]
      }
  
      cursor.execute(query, params)
      for row in cursor.fetchall():
        # pyformat: disable
        (stat_entry_path_id_bytes, stat_entry_bytes, stat_entry_timestamp,
         hash_entry_path_id_bytes, hash_entry_bytes, hash_entry_timestamp) = row
        # pyformat: enable
  
        path_id_bytes = stat_entry_path_id_bytes or hash_entry_path_id_bytes
        path_id = rdf_objects.PathID.FromBytes(path_id_bytes)
        components = path_id_components[path_id]
  
        timestamp = stat_entry_timestamp or hash_entry_timestamp
  
        if stat_entry_bytes is not None:
          stat_entry = rdf_client_fs.StatEntry.FromSerializedString(
              stat_entry_bytes)
        else:
          stat_entry = None
  
        if hash_entry_bytes is not None:
          hash_entry = rdf_crypto.Hash.FromSerializedString(hash_entry_bytes)
        else:
          hash_entry = None
  
        path_info = rdf_objects.PathInfo(
            path_type=path_type,
            components=components,
            stat_entry=stat_entry,
            hash_entry=hash_entry,
            timestamp=mysql_utils.TimestampToRDFDatetime(timestamp))
  
        path_infos[components].append(path_info)
  
      for components in components_list:
        path_infos[components].sort(key=lambda path_info: path_info.timestamp)
  
      return path_infos