1 <?php
2
3 /**
4 * ArangoDB PHP client: result set cursor
5 *
6 * @package triagens\ArangoDb
7 * @author Jan Steemann
8 * @copyright Copyright 2012, triagens GmbH, Cologne, Germany
9 */
10
11 namespace triagens\ArangoDb;
12
13 /**
14 * Provides access to the results of an AQL query or another statement
15 *
16 * The cursor might not contain all results in the beginning.<br>
17 *
18 * If the result set is too big to be transferred in one go, the
19 * cursor might issue additional HTTP requests to fetch the
20 * remaining results from the server.
21 *
22 * @package triagens\ArangoDb
23 * @since 0.2
24 */
25 class Cursor implements
26 \Iterator
27 {
28 /**
29 * The connection object
30 *
31 * @var Connection
32 */
33 private $_connection;
34 /**
35 * Cursor options
36 *
37 * @var array
38 */
39 private $_options;
40
41 /**
42 * Result Data
43 *
44 * @var array
45 */
46 private $data;
47
48 /**
49 * The result set
50 *
51 * @var array
52 */
53 private $_result;
54
55 /**
56 * "has more" indicator - if true, the server has more results
57 *
58 * @var bool
59 */
60 private $_hasMore;
61
62 /**
63 * cursor id - might be NULL if cursor does not have an id
64 *
65 * @var mixed
66 */
67 private $_id;
68
69 /**
70 * current position in result set iteration (zero-based)
71 *
72 * @var int
73 */
74 private $_position;
75
76 /**
77 * total length of result set (in number of documents)
78 *
79 * @var int
80 */
81 private $_length;
82
83 /**
84 * full count of the result set (ignoring the outermost LIMIT)
85 *
86 * @var int
87 */
88 private $_fullCount;
89
90 /**
91 * extra data (statistics) returned from the statement
92 *
93 * @var array
94 */
95 private $_extra;
96
97 /**
98 * number of HTTP calls that were made to build the cursor result
99 */
100 private $_fetches = 1;
101
102 /**
103 * whether or not the query result was served from the AQL query result cache
104 */
105 private $_cached;
106
107 /**
108 * result entry for cursor id
109 */
110 const ENTRY_ID = 'id';
111
112 /**
113 * result entry for "hasMore" flag
114 */
115 const ENTRY_HASMORE = 'hasMore';
116
117 /**
118 * result entry for result documents
119 */
120 const ENTRY_RESULT = 'result';
121
122 /**
123 * result entry for extra data
124 */
125 const ENTRY_EXTRA = 'extra';
126
127 /**
128 * result entry for stats
129 */
130 const ENTRY_STATS = 'stats';
131
132 /**
133 * result entry for the full count (ignoring the outermost LIMIT)
134 */
135 const FULL_COUNT = 'fullCount';
136
137 /**
138 * cache option entry
139 */
140 const ENTRY_CACHE = 'cache';
141
142 /**
143 * cached result attribute - whether or not the result was served from the AQL query cache
144 */
145 const ENTRY_CACHED = 'cached';
146
147 /**
148 * sanitize option entry
149 */
150 const ENTRY_SANITIZE = '_sanitize';
151
152 /**
153 * "flat" option entry (will treat the results as a simple array, not documents)
154 */
155 const ENTRY_FLAT = '_flat';
156
157 /**
158 * "objectType" option entry.
159 */
160 const ENTRY_TYPE = 'objectType';
161
162 /**
163 * "baseurl" option entry.
164 */
165 const ENTRY_BASEURL = 'baseurl';
166
167 /**
168 * Initialise the cursor with the first results and some metadata
169 *
170 * @param Connection $connection - connection to be used
171 * @param array $data - initial result data as returned by the server
172 * @param array $options - cursor options
173 *
174 * @throws \triagens\ArangoDb\ClientException
175 */
176 public function __construct(Connection $connection, array $data, array $options)
177 {
178 $this->_connection = $connection;
179 $this->data = $data;
180 $this->_id = null;
181 $this->_extra = [];
182 $this->_cached = false;
183
184 if (isset($data[self::ENTRY_ID])) {
185 $this->_id = $data[self::ENTRY_ID];
186 }
187
188 if (isset($data[self::ENTRY_EXTRA])) {
189 // ArangoDB 2.3+ return value struct
190 $this->_extra = $data[self::ENTRY_EXTRA];
191
192 if (isset($this->_extra[self::ENTRY_STATS][self::FULL_COUNT])) {
193 $this->_fullCount = $this->_extra[self::ENTRY_STATS][self::FULL_COUNT];
194 }
195 } else if (isset($data[self::ENTRY_EXTRA][self::FULL_COUNT])) {
196 // pre-ArangoDB 2.3 return value struct
197 $this->_fullCount = $data[self::ENTRY_EXTRA][self::FULL_COUNT];
198 }
199
200 if (isset($data[self::ENTRY_CACHED])) {
201 $this->_cached = $data[self::ENTRY_CACHED];
202 }
203
204 // attribute must be there
205 assert(isset($data[self::ENTRY_HASMORE]));
206 $this->_hasMore = (bool) $data[self::ENTRY_HASMORE];
207
208 $options['isNew'] = false;
209 $this->_options = $options;
210 $this->_result = [];
211 $this->add((array) $data[self::ENTRY_RESULT]);
212 $this->updateLength();
213
214 $this->rewind();
215 }
216
217
218 /**
219 * Explicitly delete the cursor
220 *
221 * This might issue an HTTP DELETE request to inform the server about
222 * the deletion.
223 *
224 * @throws Exception
225 * @return bool - true if the server acknowledged the deletion request, false otherwise
226 */
227 public function delete()
228 {
229 if ($this->_id) {
230 try {
231 $this->_connection->delete($this->url() . '/' . $this->_id, []);
232
233 return true;
234 } catch (Exception $e) {
235 }
236 }
237
238 return false;
239 }
240
241
242 /**
243 * Get the total number of results in the cursor
244 *
245 * This might issue additional HTTP requests to fetch any outstanding
246 * results from the server
247 *
248 * @throws Exception
249 * @return int - total number of results
250 */
251 public function getCount()
252 {
253 while ($this->_hasMore) {
254 $this->fetchOutstanding();
255 }
256
257 return $this->_length;
258 }
259
260 /**
261 * Get the full count of the cursor (ignoring the outermost LIMIT)
262 *
263 * @return int - total number of results
264 */
265 public function getFullCount()
266 {
267 return $this->_fullCount;
268 }
269
270
271 /**
272 * Get the cached attribute for the result set
273 *
274 * @return bool - whether or not the query result was served from the AQL query cache
275 */
276 public function getCached()
277 {
278 return $this->_cached;
279 }
280
281
282 /**
283 * Get all results as an array
284 *
285 * This might issue additional HTTP requests to fetch any outstanding
286 * results from the server
287 *
288 * @throws Exception
289 * @return array - an array of all results
290 */
291 public function getAll()
292 {
293 while ($this->_hasMore) {
294 $this->fetchOutstanding();
295 }
296
297 return $this->_result;
298 }
299
300
301 /**
302 * Rewind the cursor, necessary for Iterator
303 *
304 * @return void
305 */
306 public function rewind()
307 {
308 $this->_position = 0;
309 }
310
311
312 /**
313 * Return the current result row, necessary for Iterator
314 *
315 * @return array - the current result row as an assoc array
316 */
317 public function current()
318 {
319 return $this->_result[$this->_position];
320 }
321
322
323 /**
324 * Return the index of the current result row, necessary for Iterator
325 *
326 * @return int - the current result row index
327 */
328 public function key()
329 {
330 return $this->_position;
331 }
332
333
334 /**
335 * Advance the cursor, necessary for Iterator
336 *
337 * @return void
338 */
339 public function next()
340 {
341 ++$this->_position;
342 }
343
344
345 /**
346 * Check if cursor can be advanced further, necessary for Iterator
347 *
348 * This might issue additional HTTP requests to fetch any outstanding
349 * results from the server
350 *
351 * @throws Exception
352 * @return bool - true if the cursor can be advanced further, false if cursor is at end
353 */
354 public function valid()
355 {
356 if ($this->_position <= $this->_length - 1) {
357 // we have more results than the current position is
358 return true;
359 }
360
361 if (!$this->_hasMore || !$this->_id) {
362 // we don't have more results, but the cursor is exhausted
363 return false;
364 }
365
366 // need to fetch additional results from the server
367 $this->fetchOutstanding();
368
369 return ($this->_position <= $this->_length - 1);
370 }
371
372
373 /**
374 * Create an array of results from the input array
375 *
376 * @param array $data - incoming result
377 *
378 * @return void
379 * @throws \triagens\ArangoDb\ClientException
380 */
381 private function add(array $data)
382 {
383 foreach ($this->sanitize($data) as $row) {
384 if (!is_array($row) || (isset($this->_options[self::ENTRY_FLAT]) && $this->_options[self::ENTRY_FLAT])) {
385 $this->addFlatFromArray($row);
386 } else {
387 if (!isset($this->_options['objectType'])) {
388 $this->addDocumentsFromArray($row);
389 } else {
390 switch ($this->_options['objectType']) {
391 case 'edge' :
392 $this->addEdgesFromArray($row);
393 break;
394 case 'vertex' :
395 $this->addVerticesFromArray($row);
396 break;
397 case 'path' :
398 $this->addPathsFromArray($row);
399 break;
400 case 'shortestPath' :
401 $this->addShortestPathFromArray($row);
402 break;
403 case 'distanceTo' :
404 $this->addDistanceToFromArray($row);
405 break;
406 case 'commonNeighbors' :
407 $this->addCommonNeighborsFromArray($row);
408 break;
409 case 'commonProperties' :
410 $this->addCommonPropertiesFromArray($row);
411 break;
412 case 'figure' :
413 $this->addFigureFromArray($row);
414 break;
415 default :
416 $this->addDocumentsFromArray($row);
417 break;
418 }
419 }
420 }
421 }
422 }
423
424
425 /**
426 * Create an array of results from the input array
427 *
428 * @param array $data - array of incoming results
429 *
430 * @return void
431 */
432 private function addFlatFromArray($data)
433 {
434 $this->_result[] = $data;
435 }
436
437
438 /**
439 * Create an array of documents from the input array
440 *
441 * @param array $data - array of incoming "document" arrays
442 *
443 * @return void
444 * @throws \triagens\ArangoDb\ClientException
445 */
446 private function addDocumentsFromArray(array $data)
447 {
448 $this->_result[] = Document::createFromArray($data, $this->_options);
449 }
450
451 /**
452 * Create an array of paths from the input array
453 *
454 * @param array $data - array of incoming "paths" arrays
455 *
456 * @return void
457 * @throws \triagens\ArangoDb\ClientException
458 */
459 private function addPathsFromArray(array $data)
460 {
461 $entry = [
462 'vertices' => [],
463 'edges' => [],
464 'source' => Document::createFromArray($data['source'], $this->_options),
465 'destination' => Document::createFromArray($data['destination'], $this->_options),
466 ];
467 foreach ($data['vertices'] as $v) {
468 $entry['vertices'][] = Document::createFromArray($v, $this->_options);
469 }
470 foreach ($data['edges'] as $v) {
471 $entry['edges'][] = Edge::createFromArray($v, $this->_options);
472 }
473 $this->_result[] = $entry;
474 }
475
476 /**
477 * Create an array of shortest paths from the input array
478 *
479 * @param array $data - array of incoming "paths" arrays
480 *
481 * @return void
482 * @throws \triagens\ArangoDb\ClientException
483 */
484 private function addShortestPathFromArray(array $data)
485 {
486 if (!isset($data['vertices'])) {
487 return;
488 }
489
490 $vertices = $data['vertices'];
491 $startVertex = $vertices[0];
492 $destination = $vertices[count($vertices) - 1];
493
494 $entry = [
495 'paths' => [],
496 'source' => Document::createFromArray($startVertex, $this->_options),
497 'distance' => $data['distance'],
498 'destination' => Document::createFromArray($destination, $this->_options),
499 ];
500
501 $path = [
502 'vertices' => [],
503 'edges' => []
504 ];
505
506 foreach ($data['vertices'] as $v) {
507 $path['vertices'][] = $v;
508 }
509 foreach ($data['edges'] as $v) {
510 $path['edges'][] = Edge::createFromArray($v, $this->_options);
511 }
512 $entry['paths'][] = $path;
513
514 $this->_result[] = $entry;
515 }
516
517
518 /**
519 * Create an array of distances from the input array
520 *
521 * @param array $data - array of incoming "paths" arrays
522 *
523 * @return void
524 */
525 private function addDistanceToFromArray(array $data)
526 {
527 $entry = [
528 'source' => $data['startVertex'],
529 'distance' => $data['distance'],
530 'destination' => $data['vertex']
531 ];
532 $this->_result[] = $entry;
533 }
534
535 /**
536 * Create an array of common neighbors from the input array
537 *
538 * @param array $data - array of incoming "paths" arrays
539 *
540 * @return void
541 * @throws \triagens\ArangoDb\ClientException
542 */
543 private function addCommonNeighborsFromArray(array $data)
544 {
545 $left = $data['left'];
546 $right = $data['right'];
547
548 if (!isset($this->_result[$left])) {
549 $this->_result[$left] = [];
550 }
551 if (!isset($this->_result[$left][$right])) {
552 $this->_result[$left][$right] = [];
553 }
554
555 foreach ($data['neighbors'] as $neighbor) {
556 $this->_result[$left][$right][] = Document::createFromArray($neighbor);
557 }
558 }
559
560 /**
561 * Create an array of common properties from the input array
562 *
563 * @param array $data - array of incoming "paths" arrays
564 *
565 * @return void
566 */
567 private function addCommonPropertiesFromArray(array $data)
568 {
569 $k = array_keys($data);
570 $k = $k[0];
571 $this->_result[$k] = [];
572 foreach ($data[$k] as $c) {
573 $id = $c['_id'];
574 unset($c['_id']);
575 $this->_result[$k][$id] = $c;
576 }
577 }
578
579 /**
580 * Create an array of figuresfrom the input array
581 *
582 * @param array $data - array of incoming "paths" arrays
583 *
584 * @return void
585 */
586 private function addFigureFromArray(array $data)
587 {
588 $this->_result = $data;
589 }
590
591 /**
592 * Create an array of Edges from the input array
593 *
594 * @param array $data - array of incoming "edge" arrays
595 *
596 * @return void
597 * @throws \triagens\ArangoDb\ClientException
598 */
599 private function addEdgesFromArray(array $data)
600 {
601 $this->_result[] = Edge::createFromArray($data, $this->_options);
602 }
603
604
605 /**
606 * Create an array of Vertex from the input array
607 *
608 * @param array $data - array of incoming "vertex" arrays
609 *
610 * @return void
611 * @throws \triagens\ArangoDb\ClientException
612 */
613 private function addVerticesFromArray(array $data)
614 {
615 $this->_result[] = Vertex::createFromArray($data, $this->_options);
616 }
617
618
619 /**
620 * Sanitize the result set rows
621 *
622 * This will remove the _id and _rev attributes from the results if the
623 * "sanitize" option is set
624 *
625 * @param array $rows - array of rows to be sanitized
626 *
627 * @return array - sanitized rows
628 */
629 private function sanitize(array $rows)
630 {
631 if (isset($this->_options[self::ENTRY_SANITIZE]) && $this->_options[self::ENTRY_SANITIZE]) {
632 foreach ($rows as $key => $value) {
633
634 if (is_array($value) && isset($value[Document::ENTRY_ID])) {
635 unset($rows[$key][Document::ENTRY_ID]);
636 }
637
638 if (is_array($value) && isset($value[Document::ENTRY_REV])) {
639 unset($rows[$key][Document::ENTRY_REV]);
640 }
641 }
642 }
643
644 return $rows;
645 }
646
647
648 /**
649 * Fetch outstanding results from the server
650 *
651 * @throws Exception
652 * @return void
653 */
654 private function fetchOutstanding()
655 {
656 // continuation
657 $response = $this->_connection->put($this->url() . '/' . $this->_id, '', []);
658 ++$this->_fetches;
659
660 $data = $response->getJson();
661
662 $this->_hasMore = (bool) $data[self::ENTRY_HASMORE];
663 $this->add($data[self::ENTRY_RESULT]);
664
665 if (!$this->_hasMore) {
666 // we have fetched the complete result set and can unset the id now
667 $this->_id = null;
668 }
669
670 $this->updateLength();
671 }
672
673
674 /**
675 * Set the length of the (fetched) result set
676 *
677 * @return void
678 */
679 private function updateLength()
680 {
681 $this->_length = count($this->_result);
682 }
683
684
685 /**
686 * Return the base URL for the cursor
687 *
688 * @return string
689 */
690 private function url()
691 {
692 if (isset($this->_options[self::ENTRY_BASEURL])) {
693 return $this->_options[self::ENTRY_BASEURL];
694 }
695
696 // this is the fallback
697 return Urls::URL_CURSOR;
698 }
699
700 /**
701 * Get a statistical figure value from the query result
702 *
703 * @param string $name - name of figure to return
704 *
705 * @return int
706 */
707 private function getStatValue($name)
708 {
709 if (isset($this->_extra[self::ENTRY_STATS][$name])) {
710 return $this->_extra[self::ENTRY_STATS][$name];
711 }
712
713 return 0;
714 }
715
716 /**
717 * Get MetaData of the current cursor
718 *
719 * @return array
720 */
721 public function getMetadata()
722 {
723 return $this->data;
724 }
725
726 /**
727 * Return the extra data of the query (statistics etc.). Contents of the result array
728 * depend on the type of query executed
729 *
730 * @return array
731 */
732 public function getExtra()
733 {
734 return $this->_extra;
735 }
736
737 /**
738 * Return the warnings issued during query execution
739 *
740 * @return array
741 */
742 public function getWarnings()
743 {
744 if (isset($this->_extra['warnings'])) {
745 return $this->_extra['warnings'];
746 }
747
748 return [];
749 }
750
751 /**
752 * Return the number of writes executed by the query
753 *
754 * @return int
755 */
756 public function getWritesExecuted()
757 {
758 return $this->getStatValue('writesExecuted');
759 }
760
761 /**
762 * Return the number of ignored write operations from the query
763 *
764 * @return int
765 */
766 public function getWritesIgnored()
767 {
768 return $this->getStatValue('writesIgnored');
769 }
770
771 /**
772 * Return the number of documents iterated over in full scans
773 *
774 * @return int
775 */
776 public function getScannedFull()
777 {
778 return $this->getStatValue('scannedFull');
779 }
780
781 /**
782 * Return the number of documents iterated over in index scans
783 *
784 * @return int
785 */
786 public function getScannedIndex()
787 {
788 return $this->getStatValue('scannedIndex');
789 }
790
791 /**
792 * Return the number of documents filtered by the query
793 *
794 * @return int
795 */
796 public function getFiltered()
797 {
798 return $this->getStatValue('filtered');
799 }
800
801 /**
802 * Return the number of HTTP calls that were made to build the cursor result
803 *
804 * @return int
805 */
806 public function getFetches()
807 {
808 return $this->_fetches;
809 }
810
811 /**
812 * Return the cursor id, if any
813 *
814 * @return string
815 */
816 public function getId()
817 {
818 return $this->_id;
819 }
820
821 }
822