1 <?php
2
3 /**
4 * ArangoDB PHP client: statement
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 * Container for an AQL query
15 *
16 * Optional bind parameters can be used when issuing the AQL query to separate
17 * the query from the values.
18 * Executing a query will result in a cursor being created.
19 *
20 * There is an important distinction between two different types of statements:
21 * <ul>
22 * <li> statements that produce an array of documents as their result AND<br />
23 * <li> statements that do not produce documents
24 * </ul>
25 *
26 * For example, a statement such as "FOR e IN example RETURN e" will produce
27 * an array of documents as its result. The result can be treated as an array of
28 * documents, and each document can be updated and sent back to the server by
29 * the client.<br />
30 * <br />
31 * However, the query "RETURN 1 + 1" will not produce an array of documents as
32 * its result, but an array with a single scalar value (the number 2).
33 * "2" is not a valid document so creating a document from it will fail.<br />
34 * <br />
35 * To turn the results of this query into a document, the following needs to
36 * be done:
37 * <ul>
38 * <li> modify the query to "RETURN { value: 1 + 1 }". The result will then be a
39 * an array of documents with a "value" attribute<br />
40 * <li> use the "_flat" option for the statement to indicate that you don't want
41 * to treat the statement result as an array of documents, but as a flat array
42 * </ul>
43 *
44 * @package triagens\ArangoDb
45 * @since 0.2
46 */
47 class Statement
48 {
49 /**
50 * The connection object
51 *
52 * @var Connection
53 */
54 private $_connection;
55
56 /**
57 * The bind variables and values used for the statement
58 *
59 * @var BindVars
60 */
61 private $_bindVars;
62
63 /**
64 * The current batch size (number of result documents retrieved per round-trip)
65 *
66 * @var mixed
67 */
68 private $_batchSize;
69
70 /**
71 * The count flag (should server return total number of results)
72 *
73 * @var bool
74 */
75 private $_doCount = false;
76
77 /**
78 * The count flag (should server return total number of results ignoring the limit)
79 * Be careful! This option also prevents ArangoDB from using some server side optimizations!
80 *
81 * @var bool
82 */
83 private $_fullCount = false;
84
85 /**
86 * The query string
87 *
88 * @var string
89 */
90 private $_query;
91
92 /**
93 * "flat" flag (if set, the query results will be treated as a simple array, not documents)
94 *
95 * @var bool
96 */
97 private $_flat = false;
98
99 /**
100 * Sanitation flag (if set, the _id and _rev attributes will be removed from the results)
101 *
102 * @var bool
103 */
104 private $_sanitize = false;
105
106 /**
107 * Number of retries in case a deadlock occurs
108 *
109 * @var bool
110 */
111 private $_retries = 0;
112
113 /**
114 * Whether or not the query cache should be consulted
115 *
116 * @var bool
117 */
118 private $_cache;
119
120 /**
121 * resultType
122 *
123 * @var string
124 */
125 private $resultType;
126
127
128 /**
129 * Query string index
130 */
131 const ENTRY_QUERY = 'query';
132
133 /**
134 * Count option index
135 */
136 const ENTRY_COUNT = 'count';
137
138 /**
139 * Batch size index
140 */
141 const ENTRY_BATCHSIZE = 'batchSize';
142
143 /**
144 * Retries index
145 */
146 const ENTRY_RETRIES = 'retries';
147
148 /**
149 * Bind variables index
150 */
151 const ENTRY_BINDVARS = 'bindVars';
152
153 /**
154 * Full count option index
155 */
156 const FULL_COUNT = 'fullCount';
157
158 /**
159 * Initialise the statement
160 *
161 * The $data property can be used to specify the query text and further
162 * options for the query.
163 *
164 * An important consideration when creating a statement is whether the
165 * statement will produce a list of documents as its result or any other
166 * non-document value. When a statement is created, by default it is
167 * assumed that the statement will produce documents. If this is not the
168 * case, executing a statement that returns non-documents will fail.
169 *
170 * To explicitly mark the statement as returning non-documents, the '_flat'
171 * option should be specified in $data.
172 *
173 * @throws Exception
174 *
175 * @param Connection $connection - the connection to be used
176 * @param array $data - statement initialization data
177 */
178 public function __construct(Connection $connection, array $data)
179 {
180 $this->_connection = $connection;
181 $this->_bindVars = new BindVars();
182
183 if (isset($data[self::ENTRY_QUERY])) {
184 $this->setQuery($data[self::ENTRY_QUERY]);
185 }
186
187 if (isset($data[self::ENTRY_COUNT])) {
188 $this->setCount($data[self::ENTRY_COUNT]);
189 }
190
191 if (isset($data[self::ENTRY_BATCHSIZE])) {
192 $this->setBatchSize($data[self::ENTRY_BATCHSIZE]);
193 }
194
195 if (isset($data[self::ENTRY_BINDVARS])) {
196 $this->_bindVars->set($data[self::ENTRY_BINDVARS]);
197 }
198
199 if (isset($data[self::FULL_COUNT])) {
200 $this->_fullCount = (bool) $data[Cursor::FULL_COUNT];
201 }
202
203 if (isset($data[Cursor::ENTRY_SANITIZE])) {
204 $this->_sanitize = (bool) $data[Cursor::ENTRY_SANITIZE];
205 }
206
207 if (isset($data[self::ENTRY_RETRIES])) {
208 $this->_retries = (int) $data[self::ENTRY_RETRIES];
209 }
210
211 if (isset($data[Cursor::ENTRY_FLAT])) {
212 $this->_flat = (bool) $data[Cursor::ENTRY_FLAT];
213 }
214
215 if (isset($data[Cursor::ENTRY_CACHE])) {
216 $this->_cache = (bool) $data[Cursor::ENTRY_CACHE];
217 }
218 }
219
220 /**
221 * Return the connection object
222 *
223 * @return Connection - the connection object
224 */
225 protected function getConnection()
226 {
227 return $this->_connection;
228 }
229
230 /**
231 * Execute the statement
232 *
233 * This will post the query to the server and return the results as
234 * a Cursor. The cursor can then be used to iterate the results.
235 *
236 * @throws Exception
237 * @return Cursor
238 */
239 public function execute()
240 {
241 if (!is_string($this->_query)) {
242 throw new ClientException('Query should be a string');
243 }
244
245 $data = $this->buildData();
246
247 $tries = 0;
248 while (true) {
249 try {
250 $response = $this->_connection->post(Urls::URL_CURSOR, $this->getConnection()->json_encode_wrapper($data), []);
251
252 return new Cursor($this->_connection, $response->getJson(), $this->getCursorOptions());
253 } catch (ServerException $e) {
254 if ($tries++ >= $this->_retries) {
255 throw $e;
256 }
257 if ($e->getServerCode() !== 29) {
258 // 29 is "deadlock detected"
259 throw $e;
260 }
261 // try again
262 }
263 }
264 }
265
266
267 /**
268 * Explain the statement's execution plan
269 *
270 * This will post the query to the server and return the execution plan as an array.
271 *
272 * @throws Exception
273 * @return array
274 */
275 public function explain()
276 {
277 $data = $this->buildData();
278 $response = $this->_connection->post(Urls::URL_EXPLAIN, $this->getConnection()->json_encode_wrapper($data), []);
279
280 return $response->getJson();
281 }
282
283
284 /**
285 * Validates the statement
286 *
287 * This will post the query to the server for validation and return the validation result as an array.
288 *
289 * @throws Exception
290 * @return array
291 */
292 public function validate()
293 {
294 $data = $this->buildData();
295 $response = $this->_connection->post(Urls::URL_QUERY, $this->getConnection()->json_encode_wrapper($data), []);
296
297 return $response->getJson();
298 }
299
300
301 /**
302 * Invoke the statement
303 *
304 * This will simply call execute(). Arguments are ignored.
305 *
306 * @throws Exception
307 *
308 * @param mixed $args - arguments for invocation, will be ignored
309 *
310 * @return Cursor - the result cursor
311 */
312 public function __invoke($args)
313 {
314 return $this->execute();
315 }
316
317 /**
318 * Return a string representation of the statement
319 *
320 * @return string - the current query string
321 */
322 public function __toString()
323 {
324 return $this->_query;
325 }
326
327 /**
328 * Bind a parameter to the statement
329 *
330 * This method can either be called with a string $key and a
331 * separate value in $value, or with an array of all bind
332 * bind parameters in $key, with $value being NULL.
333 *
334 * Allowed value types for bind parameters are string, int,
335 * double, bool and array. Arrays must not contain any other
336 * than these types.
337 *
338 * @throws Exception
339 *
340 * @param mixed $key - name of bind variable OR an array of all bind variables
341 * @param mixed $value - value for bind variable
342 *
343 * @return void
344 */
345 public function bind($key, $value = null)
346 {
347 $this->_bindVars->set($key, $value);
348 }
349
350 /**
351 * Get all bind parameters as an array
352 *
353 * @return array - array of bind variables/values
354 */
355 public function getBindVars()
356 {
357 return $this->_bindVars->getAll();
358 }
359
360 /**
361 * Set the query string
362 *
363 * @throws ClientException
364 *
365 * @param string $query - query string
366 *
367 * @return void
368 */
369 public function setQuery($query)
370 {
371 if (!is_string($query)) {
372 throw new ClientException('Query should be a string');
373 }
374
375 $this->_query = $query;
376 }
377
378 /**
379 * Get the query string
380 *
381 * @return string - current query string value
382 */
383 public function getQuery()
384 {
385 return $this->_query;
386 }
387
388 /**
389 * setResultType
390 *
391 * @param $resultType
392 *
393 * @return string - resultType of the query
394 */
395 public function setResultType($resultType)
396 {
397 return $this->resultType = $resultType;
398 }
399
400 /**
401 * Set the count option for the statement
402 *
403 * @param bool $value - value for count option
404 *
405 * @return void
406 */
407 public function setCount($value)
408 {
409 $this->_doCount = (bool) $value;
410 }
411
412 /**
413 * Get the count option value of the statement
414 *
415 * @return bool - current value of count option
416 */
417 public function getCount()
418 {
419 return $this->_doCount;
420 }
421
422 /**
423 * Set the full count option for the statement
424 *
425 * @param bool $value - value for full count option
426 *
427 * @return void
428 */
429 public function setFullCount($value)
430 {
431 $this->_fullCount = (bool) $value;
432 }
433
434 /**
435 * Get the full count option value of the statement
436 *
437 * @return bool - current value of full count option
438 */
439 public function getFullCount()
440 {
441 return $this->_fullCount;
442 }
443
444 /**
445 * Set the caching option for the statement
446 *
447 * @param bool $value - value for 'cache' option
448 *
449 * @return void
450 */
451 public function setCache($value)
452 {
453 $this->_cache = (bool) $value;
454 }
455
456 /**
457 * Get the caching option value of the statement
458 *
459 * @return bool - current value of 'cache' option
460 */
461 public function getCache()
462 {
463 return $this->_cache;
464 }
465
466 /**
467 * Set the batch size for the statement
468 *
469 * The batch size is the number of results to be transferred
470 * in one server round-trip. If a query produces more results
471 * than the batch size, it creates a server-side cursor that
472 * provides the additional results.
473 *
474 * The server-side cursor can be accessed by the client with subsequent HTTP requests.
475 *
476 * @throws ClientException
477 *
478 * @param int $value - batch size value
479 *
480 * @return void
481 */
482 public function setBatchSize($value)
483 {
484 if (!is_int($value) || (int) $value <= 0) {
485 throw new ClientException('Batch size should be a positive integer');
486 }
487
488 $this->_batchSize = (int) $value;
489 }
490
491 /**
492 * Get the batch size for the statement
493 *
494 * @return int - current batch size value
495 */
496 public function getBatchSize()
497 {
498 return $this->_batchSize;
499 }
500
501
502 /**
503 * Build an array of data to be posted to the server when issuing the statement
504 *
505 * @return array - array of data to be sent to server
506 */
507 private function buildData()
508 {
509 $data = [
510 self::ENTRY_QUERY => $this->_query,
511 self::ENTRY_COUNT => $this->_doCount,
512 'options' => [
513 self::FULL_COUNT => $this->_fullCount
514 ]
515 ];
516
517 if ($this->_cache !== null) {
518 $data[Cursor::ENTRY_CACHE] = $this->_cache;
519 }
520
521 if ($this->_bindVars->getCount() > 0) {
522 $data[self::ENTRY_BINDVARS] = $this->_bindVars->getAll();
523 }
524
525 if ($this->_batchSize > 0) {
526 $data[self::ENTRY_BATCHSIZE] = $this->_batchSize;
527 }
528
529 return $data;
530 }
531
532 /**
533 * Return an array of cursor options
534 *
535 * @return array - array of options
536 */
537 private function getCursorOptions()
538 {
539 $result = [
540 Cursor::ENTRY_SANITIZE => (bool) $this->_sanitize,
541 Cursor::ENTRY_FLAT => (bool) $this->_flat,
542 Cursor::ENTRY_BASEURL => Urls::URL_CURSOR
543 ];
544 if (null !== $this->resultType) {
545 $result[Cursor::ENTRY_TYPE] = $this->resultType;
546 }
547
548 return $result;
549 }
550 }
551