Apache GraphAr C++ Library
The C++ Library for Apache GraphAr
graph_reader.cc
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements. See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership. The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License. You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied. See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 
20 #include "graphar/high-level/graph_reader.h"
21 #include "graphar/convert_to_arrow_type.h"
22 #include "graphar/types.h"
23 
24 namespace graphar {
25 
26 template <Type type>
27 Status CastToAny(std::shared_ptr<arrow::Array> array,
28  std::any& any) { // NOLINT
29  if (array->IsNull(0)) {
30  any = std::any();
31  return Status::OK();
32  }
33  using ArrayType = typename TypeToArrowType<type>::ArrayType;
34  auto column = std::dynamic_pointer_cast<ArrayType>(array);
35  any = column->GetView(0);
36  return Status::OK();
37 }
38 
39 template <>
40 Status CastToAny<Type::STRING>(std::shared_ptr<arrow::Array> array,
41  std::any& any) { // NOLINT
42  using ArrayType = typename TypeToArrowType<Type::STRING>::ArrayType;
43  auto column = std::dynamic_pointer_cast<ArrayType>(array);
44  any = column->GetString(0);
45  return Status::OK();
46 }
47 
48 Status TryToCastToAny(const std::shared_ptr<DataType>& type,
49  std::shared_ptr<arrow::Array> array,
50  std::any& any) { // NOLINT
51  switch (type->id()) {
52  case Type::BOOL:
53  return CastToAny<Type::BOOL>(array, any);
54  case Type::INT32:
55  return CastToAny<Type::INT32>(array, any);
56  case Type::INT64:
57  return CastToAny<Type::INT64>(array, any);
58  case Type::FLOAT:
59  return CastToAny<Type::FLOAT>(array, any);
60  case Type::DOUBLE:
61  return CastToAny<Type::DOUBLE>(array, any);
62  case Type::STRING:
63  return CastToAny<Type::STRING>(array, any);
64  case Type::DATE:
65  return CastToAny<Type::DATE>(array, any);
66  case Type::TIMESTAMP:
67  return CastToAny<Type::TIMESTAMP>(array, any);
68  default:
69  return Status::TypeError("Unsupported type.");
70  }
71  return Status::OK();
72 }
73 
74 Vertex::Vertex(IdType id,
75  std::vector<VertexPropertyArrowChunkReader>& readers) // NOLINT
76  : id_(id) {
77  // get the first row of table
78  for (auto& reader : readers) {
79  GAR_ASSIGN_OR_RAISE_ERROR(auto chunk_table, reader.GetChunk());
80  auto schema = chunk_table->schema();
81  for (int i = 0; i < schema->num_fields(); ++i) {
82  auto field = chunk_table->field(i);
83  if (field->type()->id() == arrow::Type::LIST) {
84  auto list_array = std::dynamic_pointer_cast<arrow::ListArray>(
85  chunk_table->column(i)->chunk(0));
86  list_properties_[field->name()] = list_array->value_slice(0);
87  } else {
88  auto type = DataType::ArrowDataTypeToDataType(field->type());
89  GAR_RAISE_ERROR_NOT_OK(TryToCastToAny(type,
90  chunk_table->column(i)->chunk(0),
91  properties_[field->name()]));
92  }
93  }
94  }
95 }
96 
97 template <typename T>
98 Result<T> Vertex::property(const std::string& property) const {
99  if constexpr (std::is_final<T>::value) {
100  auto it = list_properties_.find(property);
101  if (it == list_properties_.end()) {
102  return Status::KeyError("The list property ", property,
103  " doesn't exist.");
104  }
105  auto array = std::dynamic_pointer_cast<
107  it->second);
108  const typename T::ValueType* values = array->raw_values();
109  return T(values, array->length());
110  } else {
111  if (properties_.find(property) == properties_.end()) {
112  return Status::KeyError("Property with name ", property,
113  " does not exist in the vertex.");
114  }
115  try {
116  if (!properties_.at(property).has_value())
117  return Status::TypeError("The value of the ", property, " is null.");
118  T ret = std::any_cast<T>(properties_.at(property));
119  return ret;
120  } catch (const std::bad_any_cast& e) {
121  return Status::TypeError("Any cast failed, the property type of ",
122  property, " is not matched ", e.what());
123  }
124  }
125 }
126 
127 template <>
128 Result<Date> Vertex::property(const std::string& property) const {
129  if (properties_.find(property) == properties_.end()) {
130  return Status::KeyError("Property with name ", property,
131  " does not exist in the vertex.");
132  }
133  try {
134  if (!properties_.at(property).has_value())
135  return Status::TypeError("The value of the ", property, " is null.");
136  Date ret(std::any_cast<Date::c_type>(properties_.at(property)));
137  return ret;
138  } catch (const std::bad_any_cast& e) {
139  return Status::TypeError("Any cast failed, the property type of ", property,
140  " is not matched ", e.what());
141  }
142 }
143 
144 template <>
145 Result<Timestamp> Vertex::property(const std::string& property) const {
146  if (properties_.find(property) == properties_.end()) {
147  return Status::KeyError("Property with name ", property,
148  " does not exist in the vertex.");
149  }
150  try {
151  if (!properties_.at(property).has_value())
152  return Status::TypeError("The value of the ", property, " is null.");
153  Timestamp ret(std::any_cast<Timestamp::c_type>(properties_.at(property)));
154  return ret;
155  } catch (const std::bad_any_cast& e) {
156  return Status::TypeError("Any cast failed, the property type of ", property,
157  " is not matched ", e.what());
158  }
159 }
160 
161 template <>
162 Result<StringArray> Vertex::property(const std::string& property) const {
163  auto it = list_properties_.find(property);
164  if (it == list_properties_.end()) {
165  return Status::KeyError("The list property ", property, " doesn't exist.");
166  }
167  auto array = std::dynamic_pointer_cast<arrow::StringArray>(it->second);
168  return StringArray(array->raw_value_offsets(), array->raw_data(),
169  array->length());
170 }
171 
173  AdjListArrowChunkReader& adj_list_reader, // NOLINT
174  std::vector<AdjListPropertyArrowChunkReader>& property_readers) { // NOLINT
175  // get the first row of table
176  GAR_ASSIGN_OR_RAISE_ERROR(auto adj_list_chunk_table,
177  adj_list_reader.GetChunk());
178  src_id_ = std::dynamic_pointer_cast<arrow::Int64Array>(
179  adj_list_chunk_table->column(0)->chunk(0))
180  ->GetView(0);
181  dst_id_ = std::dynamic_pointer_cast<arrow::Int64Array>(
182  adj_list_chunk_table->column(1)->chunk(0))
183  ->GetView(0);
184  for (auto& reader : property_readers) {
185  // get the first row of table
186  GAR_ASSIGN_OR_RAISE_ERROR(auto chunk_table, reader.GetChunk());
187  auto schema = chunk_table->schema();
188  for (int i = 0; i < schema->num_fields(); ++i) {
189  auto field = chunk_table->field(i);
190  if (field->type()->id() == arrow::Type::LIST) {
191  auto list_array = std::dynamic_pointer_cast<arrow::ListArray>(
192  chunk_table->column(i)->chunk(0));
193  list_properties_[field->name()] = list_array->value_slice(0);
194  } else {
195  auto type = DataType::ArrowDataTypeToDataType(field->type());
196  GAR_RAISE_ERROR_NOT_OK(TryToCastToAny(type,
197  chunk_table->column(i)->chunk(0),
198  properties_[field->name()]));
199  }
200  }
201  }
202 }
203 
204 template <typename T>
205 Result<T> Edge::property(const std::string& property) const {
206  if constexpr (std::is_final<T>::value) {
207  auto it = list_properties_.find(property);
208  if (it == list_properties_.end()) {
209  return Status::KeyError("The list property ", property,
210  " doesn't exist.");
211  }
212  auto array = std::dynamic_pointer_cast<
214  it->second);
215  const typename T::ValueType* values = array->raw_values();
216  return T(values, array->length());
217  } else {
218  if (properties_.find(property) == properties_.end()) {
219  return Status::KeyError("Property with name ", property,
220  " does not exist in the edge.");
221  }
222  try {
223  if (!properties_.at(property).has_value())
224  return Status::TypeError("The value of the ", property, " is null.");
225  T ret = std::any_cast<T>(properties_.at(property));
226  return ret;
227  } catch (const std::bad_any_cast& e) {
228  return Status::TypeError("Any cast failed, the property type of ",
229  property, " is not matched ", e.what());
230  }
231  }
232 }
233 
234 template <>
235 Result<Date> Edge::property(const std::string& property) const {
236  if (properties_.find(property) == properties_.end()) {
237  return Status::KeyError("Property with name ", property,
238  " does not exist in the edge.");
239  }
240  try {
241  if (!properties_.at(property).has_value())
242  return Status::TypeError("The value of the ", property, " is null.");
243  Date ret(std::any_cast<Date::c_type>(properties_.at(property)));
244  return ret;
245  } catch (const std::bad_any_cast& e) {
246  return Status::TypeError("Any cast failed, the property type of ", property,
247  " is not matched ", e.what());
248  }
249 }
250 
251 template <>
252 Result<Timestamp> Edge::property(const std::string& property) const {
253  if (properties_.find(property) == properties_.end()) {
254  return Status::KeyError("Property with name ", property,
255  " does not exist in the edge.");
256  }
257  try {
258  if (!properties_.at(property).has_value())
259  return Status::TypeError("The value of the ", property, " is null.");
260  Timestamp ret(std::any_cast<Timestamp::c_type>(properties_.at(property)));
261  return ret;
262  } catch (const std::bad_any_cast& e) {
263  return Status::TypeError("Any cast failed, the property type of ", property,
264  " is not matched ", e.what());
265  }
266 }
267 
268 template <>
269 Result<StringArray> Edge::property(const std::string& property) const {
270  auto it = list_properties_.find(property);
271  if (it == list_properties_.end()) {
272  return Status::KeyError("The list property ", property, " doesn't exist.");
273  }
274  auto array = std::dynamic_pointer_cast<arrow::StringArray>(it->second);
275  return StringArray(array->raw_value_offsets(), array->raw_data(),
276  array->length());
277 }
278 
279 #define INSTANTIATE_PROPERTY(T) \
280  template Result<T> Vertex::property<T>(const std::string& name) const; \
281  template Result<T> Edge::property<T>(const std::string& name) const;
282 
283 INSTANTIATE_PROPERTY(bool)
284 INSTANTIATE_PROPERTY(const bool&)
285 INSTANTIATE_PROPERTY(int32_t)
286 INSTANTIATE_PROPERTY(const int32_t&)
287 INSTANTIATE_PROPERTY(Int32Array)
288 INSTANTIATE_PROPERTY(int64_t)
289 INSTANTIATE_PROPERTY(const int64_t&)
290 INSTANTIATE_PROPERTY(Int64Array)
291 INSTANTIATE_PROPERTY(float)
292 INSTANTIATE_PROPERTY(const float&)
293 INSTANTIATE_PROPERTY(FloatArray)
294 INSTANTIATE_PROPERTY(double)
295 INSTANTIATE_PROPERTY(const double&)
296 INSTANTIATE_PROPERTY(DoubleArray)
297 INSTANTIATE_PROPERTY(std::string)
298 INSTANTIATE_PROPERTY(const std::string&)
299 
300 IdType EdgeIter::source() {
301  adj_list_reader_.seek(cur_offset_);
302  GAR_ASSIGN_OR_RAISE_ERROR(auto chunk, adj_list_reader_.GetChunk());
303  auto src_column = chunk->column(0);
304  return std::dynamic_pointer_cast<arrow::Int64Array>(src_column->chunk(0))
305  ->GetView(0);
306 }
307 
309  adj_list_reader_.seek(cur_offset_);
310  GAR_ASSIGN_OR_RAISE_ERROR(auto chunk, adj_list_reader_.GetChunk());
311  auto src_column = chunk->column(1);
312  return std::dynamic_pointer_cast<arrow::Int64Array>(src_column->chunk(0))
313  ->GetView(0);
314 }
315 
316 bool EdgeIter::first_src(const EdgeIter& from, IdType id) {
317  if (from.is_end())
318  return false;
319 
320  // ordered_by_dest or unordered_by_dest
321  if (adj_list_type_ == AdjListType::ordered_by_dest ||
322  adj_list_type_ == AdjListType::unordered_by_dest) {
323  if (from.global_chunk_index_ >= chunk_end_) {
324  return false;
325  }
326  if (from.global_chunk_index_ == global_chunk_index_) {
327  cur_offset_ = from.cur_offset_;
328  } else if (from.global_chunk_index_ < chunk_begin_) {
329  this->to_begin();
330  } else {
331  global_chunk_index_ = from.global_chunk_index_;
332  cur_offset_ = from.cur_offset_;
333  vertex_chunk_index_ = from.vertex_chunk_index_;
334  this->refresh();
335  }
336  while (!this->is_end()) {
337  if (this->source() == id)
338  return true;
339  this->operator++();
340  }
341  return false;
342  }
343 
344  // unordered_by_source
345  if (adj_list_type_ == AdjListType::unordered_by_source) {
346  IdType expect_chunk_index =
347  index_converter_->IndexPairToGlobalChunkIndex(id / src_chunk_size_, 0);
348  if (expect_chunk_index > chunk_end_)
349  return false;
350  if (from.global_chunk_index_ >= chunk_end_) {
351  return false;
352  }
353  bool need_refresh = false;
354  if (from.global_chunk_index_ == global_chunk_index_) {
355  cur_offset_ = from.cur_offset_;
356  } else if (from.global_chunk_index_ < chunk_begin_) {
357  this->to_begin();
358  } else {
359  global_chunk_index_ = from.global_chunk_index_;
360  cur_offset_ = from.cur_offset_;
361  vertex_chunk_index_ = from.vertex_chunk_index_;
362  need_refresh = true;
363  }
364  if (global_chunk_index_ < expect_chunk_index) {
365  global_chunk_index_ = expect_chunk_index;
366  cur_offset_ = 0;
367  vertex_chunk_index_ = id / src_chunk_size_;
368  need_refresh = true;
369  }
370  if (need_refresh)
371  this->refresh();
372  while (!this->is_end()) {
373  if (this->source() == id)
374  return true;
375  if (vertex_chunk_index_ > id / src_chunk_size_)
376  return false;
377  this->operator++();
378  }
379  return false;
380  }
381 
382  // ordered_by_source
383  auto st = offset_reader_->seek(id);
384  if (!st.ok()) {
385  return false;
386  }
387  auto maybe_offset_chunk = offset_reader_->GetChunk();
388  if (!maybe_offset_chunk.status().ok()) {
389  return false;
390  }
391  auto offset_array =
392  std::dynamic_pointer_cast<arrow::Int64Array>(maybe_offset_chunk.value());
393  auto begin_offset = static_cast<IdType>(offset_array->Value(0));
394  auto end_offset = static_cast<IdType>(offset_array->Value(1));
395  if (begin_offset >= end_offset) {
396  return false;
397  }
398  auto vertex_chunk_index_of_id = offset_reader_->GetChunkIndex();
399  auto begin_global_index = index_converter_->IndexPairToGlobalChunkIndex(
400  vertex_chunk_index_of_id, begin_offset / chunk_size_);
401  auto end_global_index = index_converter_->IndexPairToGlobalChunkIndex(
402  vertex_chunk_index_of_id, end_offset / chunk_size_);
403  if (begin_global_index <= from.global_chunk_index_ &&
404  from.global_chunk_index_ <= end_global_index) {
405  if (begin_offset < from.cur_offset_ && from.cur_offset_ < end_offset) {
406  global_chunk_index_ = from.global_chunk_index_;
407  cur_offset_ = from.cur_offset_;
408  vertex_chunk_index_ = from.vertex_chunk_index_;
409  refresh();
410  return true;
411  } else if (from.cur_offset_ <= begin_offset) {
412  global_chunk_index_ = begin_global_index;
413  cur_offset_ = begin_offset;
414  vertex_chunk_index_ = vertex_chunk_index_of_id;
415  refresh();
416  return true;
417  } else {
418  return false;
419  }
420  } else if (from.global_chunk_index_ < begin_global_index) {
421  global_chunk_index_ = begin_global_index;
422  cur_offset_ = begin_offset;
423  vertex_chunk_index_ = vertex_chunk_index_of_id;
424  refresh();
425  return true;
426  } else {
427  return false;
428  }
429 }
430 
431 bool EdgeIter::first_dst(const EdgeIter& from, IdType id) {
432  if (from.is_end())
433  return false;
434 
435  // ordered_by_source or unordered_by_source
436  if (adj_list_type_ == AdjListType::ordered_by_source ||
437  adj_list_type_ == AdjListType::unordered_by_source) {
438  if (from.global_chunk_index_ >= chunk_end_) {
439  return false;
440  }
441  if (from.global_chunk_index_ == global_chunk_index_) {
442  cur_offset_ = from.cur_offset_;
443  } else if (from.global_chunk_index_ < chunk_begin_) {
444  this->to_begin();
445  } else {
446  global_chunk_index_ = from.global_chunk_index_;
447  cur_offset_ = from.cur_offset_;
448  vertex_chunk_index_ = from.vertex_chunk_index_;
449  this->refresh();
450  }
451  while (!this->is_end()) {
452  if (this->destination() == id)
453  return true;
454  this->operator++();
455  }
456  return false;
457  }
458 
459  // unordered_by_dest
460  if (adj_list_type_ == AdjListType::unordered_by_dest) {
461  IdType expect_chunk_index =
462  index_converter_->IndexPairToGlobalChunkIndex(id / dst_chunk_size_, 0);
463  if (expect_chunk_index > chunk_end_)
464  return false;
465  if (from.global_chunk_index_ >= chunk_end_) {
466  return false;
467  }
468  bool need_refresh = false;
469  if (from.global_chunk_index_ == global_chunk_index_) {
470  cur_offset_ = from.cur_offset_;
471  } else if (from.global_chunk_index_ < chunk_begin_) {
472  this->to_begin();
473  } else {
474  global_chunk_index_ = from.global_chunk_index_;
475  cur_offset_ = from.cur_offset_;
476  vertex_chunk_index_ = from.vertex_chunk_index_;
477  need_refresh = true;
478  }
479  if (global_chunk_index_ < expect_chunk_index) {
480  global_chunk_index_ = expect_chunk_index;
481  cur_offset_ = 0;
482  vertex_chunk_index_ = id / dst_chunk_size_;
483  need_refresh = true;
484  }
485  if (need_refresh)
486  this->refresh();
487  while (!this->is_end()) {
488  if (this->destination() == id)
489  return true;
490  if (vertex_chunk_index_ > id / dst_chunk_size_)
491  return false;
492  this->operator++();
493  }
494  return false;
495  }
496 
497  // ordered_by_dest
498  auto st = offset_reader_->seek(id);
499  if (!st.ok()) {
500  return false;
501  }
502  auto maybe_offset_chunk = offset_reader_->GetChunk();
503  if (!maybe_offset_chunk.status().ok()) {
504  return false;
505  }
506  auto offset_array =
507  std::dynamic_pointer_cast<arrow::Int64Array>(maybe_offset_chunk.value());
508  auto begin_offset = static_cast<IdType>(offset_array->Value(0));
509  auto end_offset = static_cast<IdType>(offset_array->Value(1));
510  if (begin_offset >= end_offset) {
511  return false;
512  }
513  auto vertex_chunk_index_of_id = offset_reader_->GetChunkIndex();
514  auto begin_global_index = index_converter_->IndexPairToGlobalChunkIndex(
515  vertex_chunk_index_of_id, begin_offset / chunk_size_);
516  auto end_global_index = index_converter_->IndexPairToGlobalChunkIndex(
517  vertex_chunk_index_of_id, end_offset / chunk_size_);
518  if (begin_global_index <= from.global_chunk_index_ &&
519  from.global_chunk_index_ <= end_global_index) {
520  if (begin_offset < from.cur_offset_ && from.cur_offset_ < end_offset) {
521  global_chunk_index_ = from.global_chunk_index_;
522  cur_offset_ = from.cur_offset_;
523  vertex_chunk_index_ = from.vertex_chunk_index_;
524  refresh();
525  return true;
526  } else if (from.cur_offset_ <= begin_offset) {
527  global_chunk_index_ = begin_global_index;
528  cur_offset_ = begin_offset;
529  vertex_chunk_index_ = vertex_chunk_index_of_id;
530  refresh();
531  return true;
532  } else {
533  return false;
534  }
535  } else if (from.global_chunk_index_ < begin_global_index) {
536  global_chunk_index_ = begin_global_index;
537  cur_offset_ = begin_offset;
538  vertex_chunk_index_ = vertex_chunk_index_of_id;
539  refresh();
540  return true;
541  } else {
542  return false;
543  }
544 }
545 
546 Result<std::shared_ptr<EdgesCollection>> EdgesCollection::Make(
547  const std::shared_ptr<GraphInfo>& graph_info, const std::string& src_label,
548  const std::string& edge_label, const std::string& dst_label,
549  AdjListType adj_list_type, const IdType vertex_chunk_begin,
550  const IdType vertex_chunk_end) noexcept {
551  auto edge_info = graph_info->GetEdgeInfo(src_label, edge_label, dst_label);
552  if (!edge_info) {
553  return Status::KeyError("The edge ", src_label, " ", edge_label, " ",
554  dst_label, " doesn't exist.");
555  }
556  if (!edge_info->HasAdjacentListType(adj_list_type)) {
557  return Status::Invalid("The edge ", edge_label, " of adj list type ",
558  AdjListTypeToString(adj_list_type),
559  " doesn't exist.");
560  }
561  switch (adj_list_type) {
562  case AdjListType::ordered_by_source:
563  return std::make_shared<OBSEdgeCollection>(
564  edge_info, graph_info->GetPrefix(), vertex_chunk_begin,
565  vertex_chunk_end);
566  case AdjListType::ordered_by_dest:
567  return std::make_shared<OBDEdgesCollection>(
568  edge_info, graph_info->GetPrefix(), vertex_chunk_begin,
569  vertex_chunk_end);
570  case AdjListType::unordered_by_source:
571  return std::make_shared<UBSEdgesCollection>(
572  edge_info, graph_info->GetPrefix(), vertex_chunk_begin,
573  vertex_chunk_end);
574  case AdjListType::unordered_by_dest:
575  return std::make_shared<UBDEdgesCollection>(
576  edge_info, graph_info->GetPrefix(), vertex_chunk_begin,
577  vertex_chunk_end);
578  default:
579  return Status::Invalid("Unknown adj list type.");
580  }
581  return Status::OK();
582 }
583 
584 } // namespace graphar
The arrow chunk reader for adj list topology chunk.
Definition: chunk_reader.h:158
Status seek(IdType offset)
Sets chunk position indicator for reader by edge index.
Result< std::shared_ptr< arrow::Table > > GetChunk()
Return the current chunk of chunk position indicator as arrow::Table, if the chunk is empty,...
Edge(AdjListArrowChunkReader &adj_list_reader, std::vector< AdjListPropertyArrowChunkReader > &property_readers)
Result< T > property(const std::string &property) const
Get the property value of the edge.
The iterator for traversing a type of edges.
Definition: graph_reader.h:325
EdgeIter & operator++()
Definition: graph_reader.h:425
bool first_dst(const EdgeIter &from, IdType id)
bool first_src(const EdgeIter &from, IdType id)
bool is_end() const
Definition: graph_reader.h:542
static Result< std::shared_ptr< EdgesCollection > > Make(const std::shared_ptr< GraphInfo > &graph_info, const std::string &src_label, const std::string &edge_label, const std::string &dst_label, AdjListType adj_list_type, const IdType vertex_chunk_begin=0, const IdType vertex_chunk_end=std::numeric_limits< int64_t >::max()) noexcept
Construct an EdgesCollection from graph info and edge label.
static Status TypeError(Args &&... args)
Definition: status.h:178
static Status KeyError(Args &&... args)
Definition: status.h:172
static Status Invalid(Args &&... args)
Definition: status.h:188
static Status OK()
Definition: status.h:157
Result< T > property(const std::string &property) const
Get the property value of the vertex.
Definition: graph_reader.cc:98
Vertex(IdType id, std::vector< VertexPropertyArrowChunkReader > &readers)
Definition: graph_reader.cc:74