1 | <?php |
||
2 | |||
3 | namespace ClickHouseDB; |
||
4 | |||
5 | use ClickHouseDB\Exception\ClickHouseUnavailableException; |
||
6 | use ClickHouseDB\Exception\DatabaseException; |
||
7 | use ClickHouseDB\Exception\QueryException; |
||
8 | use ClickHouseDB\Query\Query; |
||
9 | use ClickHouseDB\Transport\CurlerRequest; |
||
10 | use ClickHouseDB\Transport\CurlerResponse; |
||
11 | |||
12 | class Statement implements \Iterator |
||
13 | { |
||
14 | private const CLICKHOUSE_ERROR_REGEX = "%Code:\s(\d+)\.\s*DB::Exception\s*:\s*(.*)(?:,\s*e\.what|\(version).*%ius"; |
||
15 | |||
16 | /** |
||
17 | * @var string|mixed |
||
18 | */ |
||
19 | private $_rawData; |
||
20 | |||
21 | /** |
||
22 | * @var int |
||
23 | */ |
||
24 | private $_http_code = -1; |
||
25 | |||
26 | /** |
||
27 | * @var CurlerRequest |
||
28 | */ |
||
29 | private $_request = null; |
||
30 | |||
31 | /** |
||
32 | * @var bool |
||
33 | */ |
||
34 | private $_init = false; |
||
35 | |||
36 | /** |
||
37 | * @var Query |
||
38 | */ |
||
39 | private $query; |
||
40 | |||
41 | /** |
||
42 | * @var mixed |
||
43 | */ |
||
44 | private $format; |
||
45 | |||
46 | /** |
||
47 | * @var string |
||
48 | */ |
||
49 | private $sql = ''; |
||
50 | |||
51 | /** |
||
52 | * @var array |
||
53 | */ |
||
54 | private $meta; |
||
55 | |||
56 | /** |
||
57 | * @var array |
||
58 | */ |
||
59 | private $totals; |
||
60 | |||
61 | /** |
||
62 | * @var array |
||
63 | */ |
||
64 | private $extremes; |
||
65 | |||
66 | /** |
||
67 | * @var int |
||
68 | */ |
||
69 | private $rows; |
||
70 | |||
71 | /** |
||
72 | * @var bool|integer |
||
73 | */ |
||
74 | private $rows_before_limit_at_least = false; |
||
75 | |||
76 | /** |
||
77 | * @var array |
||
78 | */ |
||
79 | private $array_data = []; |
||
80 | |||
81 | /** |
||
82 | * @var array|null |
||
83 | */ |
||
84 | private $statistics = null; |
||
85 | |||
86 | /** |
||
87 | * @var int |
||
88 | */ |
||
89 | public $iterator = 0; |
||
90 | 45 | ||
91 | |||
92 | 45 | public function __construct(CurlerRequest $request) |
|
93 | 45 | { |
|
94 | 45 | $this->_request = $request; |
|
95 | 45 | $this->format = $this->_request->getRequestExtendedInfo('format'); |
|
96 | 45 | $this->query = $this->_request->getRequestExtendedInfo('query'); |
|
97 | $this->sql = $this->_request->getRequestExtendedInfo('sql'); |
||
98 | } |
||
99 | |||
100 | /** |
||
101 | * @return CurlerRequest |
||
102 | */ |
||
103 | public function getRequest() |
||
104 | { |
||
105 | return $this->_request; |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * @return CurlerResponse |
||
110 | 39 | * @throws Exception\TransportException |
|
111 | */ |
||
112 | 39 | private function response() |
|
113 | { |
||
114 | return $this->_request->response(); |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * @return mixed |
||
119 | 1 | * @throws Exception\TransportException |
|
120 | */ |
||
121 | 1 | public function responseInfo() |
|
122 | { |
||
123 | return $this->response()->info(); |
||
124 | } |
||
125 | |||
126 | /** |
||
127 | 10 | * @return mixed|string |
|
128 | */ |
||
129 | 10 | public function sql() |
|
130 | { |
||
131 | return $this->sql; |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * @param string $body |
||
136 | 5 | * @return array|bool |
|
137 | */ |
||
138 | 5 | private function parseErrorClickHouse(string $body) |
|
139 | 5 | { |
|
140 | $body = trim($body); |
||
141 | $matches = []; |
||
142 | |||
143 | // Code: 115. DB::Exception: Unknown setting readonly[0], e.what() = DB::Exception |
||
144 | // Code: 192. DB::Exception: Unknown user x, e.what() = DB::Exception |
||
145 | 5 | // Code: 60. DB::Exception: Table default.ZZZZZ doesn't exist., e.what() = DB::Exception |
|
146 | 4 | // Code: 516. DB::Exception: test_username: Authentication failed: password is incorrect or there is no user with such name. (AUTHENTICATION_FAILED) (version 22.8.3.13 (official build)) |
|
147 | |||
148 | if (preg_match(self::CLICKHOUSE_ERROR_REGEX, $body, $matches)) { |
||
149 | 1 | return ['code' => $matches[1], 'message' => $matches[2]]; |
|
150 | } |
||
151 | |||
152 | return false; |
||
153 | } |
||
154 | |||
155 | private function hasErrorClickhouse(string $body): bool { |
||
156 | 8 | ||
157 | return preg_match(self::CLICKHOUSE_ERROR_REGEX, $body) === 1; |
||
158 | 8 | } |
|
159 | 2 | ||
160 | /** |
||
161 | * @return bool |
||
162 | 6 | * @throws Exception\TransportException |
|
163 | 6 | */ |
|
164 | 6 | public function error() |
|
165 | { |
||
166 | 6 | if (!$this->isError()) { |
|
167 | 6 | return false; |
|
168 | 5 | } |
|
169 | |||
170 | 5 | $body = $this->response()->body(); |
|
171 | 4 | $error_no = $this->response()->error_no(); |
|
172 | $error = $this->response()->error(); |
||
173 | 1 | ||
174 | 1 | $dumpStatement = false; |
|
175 | 1 | if (!$error_no && !$error) { |
|
176 | $parse = $this->parseErrorClickHouse($body); |
||
177 | |||
178 | 1 | if ($parse) { |
|
0 ignored issues
–
show
|
|||
179 | 1 | throw new DatabaseException($parse['message'] . "\nIN:" . $this->sql(), $parse['code']); |
|
180 | } else { |
||
181 | $code = $this->response()->http_code(); |
||
182 | 2 | $message = "HttpCode:" . $this->response()->http_code() . " ; " . $this->response()->error() . " ;" . $body; |
|
183 | 2 | $dumpStatement = true; |
|
184 | } |
||
185 | } else { |
||
186 | $code = $error_no; |
||
187 | 2 | $message = $this->response()->error(); |
|
188 | 1 | } |
|
189 | 1 | ||
190 | $exception = new QueryException($message, $code); |
||
191 | if ($code === CURLE_COULDNT_CONNECT) { |
||
192 | 2 | $exception = new ClickHouseUnavailableException($message, $code); |
|
193 | } |
||
194 | |||
195 | if ($dumpStatement) { |
||
196 | $exception->setRequestDetails($this->_request->getDetails()); |
||
197 | $exception->setResponseDetails($this->response()->getDetails()); |
||
198 | } |
||
199 | 39 | ||
200 | throw $exception; |
||
201 | 39 | } |
|
202 | |||
203 | /** |
||
204 | 32 | * @return bool |
|
205 | * @throws Exception\TransportException |
||
206 | 32 | */ |
|
207 | public function isError(): bool |
||
208 | { |
||
209 | if ($this->response()->http_code() !== 200) { |
||
210 | 32 | return true; |
|
211 | 1 | } |
|
212 | |||
213 | if ($this->response()->error_no()) { |
||
214 | 31 | return true; |
|
215 | } |
||
216 | |||
217 | if ($this->hasErrorClickhouse($this->response()->body())) { |
||
218 | return true; |
||
219 | } |
||
220 | |||
221 | 30 | return false; |
|
222 | } |
||
223 | 30 | ||
224 | private function check(): bool |
||
225 | { |
||
226 | if (!$this->_request->isResponseExists()) { |
||
227 | throw QueryException::noResponse(); |
||
228 | 30 | } |
|
229 | |||
230 | if ($this->isError()) { |
||
231 | 29 | $this->error(); |
|
232 | } |
||
233 | 29 | ||
234 | return true; |
||
235 | } |
||
236 | |||
237 | 29 | /** |
|
238 | 29 | * @return bool |
|
239 | */ |
||
240 | 29 | public function isInited() |
|
241 | 29 | { |
|
242 | return $this->_init; |
||
243 | 29 | } |
|
244 | |||
245 | /** |
||
246 | 29 | * @return bool |
|
247 | * @throws Exception\TransportException |
||
248 | */ |
||
249 | private function init() |
||
250 | { |
||
251 | if ($this->_init) { |
||
252 | 29 | return false; |
|
253 | } |
||
254 | |||
255 | $this->check(); |
||
256 | 29 | ||
257 | 29 | $this->_rawData = $this->response()->rawDataOrJson($this->format); |
|
258 | 29 | ||
259 | 29 | if (!$this->_rawData) { |
|
260 | $this->_init = true; |
||
261 | return false; |
||
262 | 29 | } |
|
263 | |||
264 | 1 | $data = []; |
|
265 | foreach (['meta', 'data', 'totals', 'extremes', 'rows', 'rows_before_limit_at_least', 'statistics'] as $key) { |
||
266 | |||
267 | 28 | if (isset($this->_rawData[$key])) { |
|
268 | 28 | if ($key=='data') { |
|
269 | $data=$this->_rawData[$key]; |
||
270 | } else { |
||
271 | $this->{$key} = $this->_rawData[$key]; |
||
272 | 29 | } |
|
273 | } |
||
274 | } |
||
275 | |||
276 | 29 | if (empty($this->meta)) { |
|
277 | throw new QueryException('Can`t find meta'); |
||
278 | } |
||
279 | |||
280 | $isJSONCompact=(stripos($this->format,'JSONCompact')!==false?true:false); |
||
281 | $this->array_data = []; |
||
282 | foreach ($data as $rows) { |
||
283 | $r = []; |
||
284 | |||
285 | if ($isJSONCompact) { |
||
286 | $r[] = $rows; |
||
287 | } else { |
||
288 | foreach ($this->meta as $meta) { |
||
289 | $r[$meta['name']] = $rows[$meta['name']]; |
||
290 | } |
||
291 | } |
||
292 | |||
293 | 1 | $this->array_data[] = $r; |
|
294 | } |
||
295 | 1 | ||
296 | 1 | $this->_init = true; |
|
297 | |||
298 | return true; |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @return array |
||
303 | * @throws \Exception |
||
304 | 1 | */ |
|
305 | public function extremes() |
||
306 | 1 | { |
|
307 | $this->init(); |
||
308 | 1 | return $this->extremes; |
|
309 | } |
||
310 | |||
311 | /** |
||
312 | 1 | * @return mixed |
|
313 | * @throws Exception\TransportException |
||
314 | */ |
||
315 | public function totalTimeRequest() |
||
316 | { |
||
317 | $this->check(); |
||
318 | return $this->response()->total_time(); |
||
319 | |||
320 | } |
||
321 | |||
322 | /** |
||
323 | * @return array |
||
324 | * @throws \Exception |
||
325 | */ |
||
326 | public function extremesMin() |
||
327 | { |
||
328 | $this->init(); |
||
329 | |||
330 | if (empty($this->extremes['min'])) { |
||
331 | return []; |
||
332 | } |
||
333 | |||
334 | 1 | return $this->extremes['min']; |
|
335 | } |
||
336 | 1 | ||
337 | 1 | /** |
|
338 | * @return array |
||
339 | * @throws \Exception |
||
340 | */ |
||
341 | public function extremesMax() |
||
342 | { |
||
343 | $this->init(); |
||
344 | |||
345 | if (empty($this->extremes['max'])) { |
||
346 | return []; |
||
347 | } |
||
348 | |||
349 | return $this->extremes['max']; |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * @return array |
||
354 | * @throws Exception\TransportException |
||
355 | */ |
||
356 | public function totals() |
||
357 | { |
||
358 | $this->init(); |
||
359 | return $this->totals; |
||
360 | } |
||
361 | 2 | ||
362 | /** |
||
363 | 2 | * |
|
364 | 2 | */ |
|
365 | public function dump() |
||
366 | { |
||
367 | $this->_request->dump(); |
||
368 | $this->response()->dump(); |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * @return bool|int |
||
373 | * @throws Exception\TransportException |
||
374 | */ |
||
375 | public function countAll() |
||
376 | { |
||
377 | $this->init(); |
||
378 | return $this->rows_before_limit_at_least; |
||
379 | } |
||
380 | |||
381 | /** |
||
382 | * @param bool $key |
||
383 | * @return array|mixed|null |
||
384 | * @throws Exception\TransportException |
||
385 | */ |
||
386 | public function statistics($key = false) |
||
387 | { |
||
388 | $this->init(); |
||
389 | |||
390 | if (!is_array($this->statistics)) { |
||
391 | return null; |
||
392 | } |
||
393 | |||
394 | 8 | if (!$key) return $this->statistics; |
|
395 | |||
396 | 8 | if (!isset($this->statistics[$key])) { |
|
397 | 8 | return null; |
|
398 | } |
||
399 | |||
400 | return $this->statistics[$key]; |
||
401 | |||
402 | } |
||
403 | |||
404 | 3 | /** |
|
405 | * @return int |
||
406 | 3 | * @throws Exception\TransportException |
|
407 | */ |
||
408 | public function count() |
||
409 | { |
||
410 | 3 | $this->init(); |
|
411 | return $this->rows; |
||
412 | 3 | } |
|
413 | |||
414 | /** |
||
415 | * @return mixed|string |
||
416 | * @throws Exception\TransportException |
||
417 | */ |
||
418 | 2 | public function rawData() |
|
419 | { |
||
420 | 2 | if ($this->_init) { |
|
421 | 2 | return $this->_rawData; |
|
422 | } |
||
423 | 2 | ||
424 | $this->check(); |
||
425 | 2 | ||
426 | return $this->response()->rawDataOrJson($this->format); |
||
427 | 2 | } |
|
428 | |||
429 | 2 | /** |
|
430 | * |
||
431 | */ |
||
432 | public function resetIterator() |
||
433 | 2 | { |
|
434 | $this->iterator=0; |
||
435 | 2 | } |
|
436 | |||
437 | public function fetchRow($key = null) |
||
438 | 2 | { |
|
439 | 1 | $this->init(); |
|
440 | |||
441 | $position=$this->iterator; |
||
442 | 2 | ||
443 | if (!isset($this->array_data[$position])) { |
||
444 | return null; |
||
445 | } |
||
446 | |||
447 | $this->iterator++; |
||
448 | |||
449 | if (!$key) { |
||
450 | 18 | return $this->array_data[$position]; |
|
451 | } |
||
452 | 18 | if (!isset($this->array_data[$position][$key])) { |
|
453 | 18 | return null; |
|
454 | } |
||
455 | |||
456 | return $this->array_data[$position][$key]; |
||
457 | 18 | ||
458 | 3 | } |
|
459 | /** |
||
460 | * @param string $key |
||
461 | 18 | * @return mixed|null |
|
462 | 2 | * @throws Exception\TransportException |
|
463 | */ |
||
464 | public function fetchOne($key = null) |
||
465 | 17 | { |
|
466 | $this->init(); |
||
467 | if (!isset($this->array_data[0])) { |
||
468 | return null; |
||
469 | } |
||
470 | |||
471 | if (!$key) { |
||
472 | return $this->array_data[0]; |
||
473 | 4 | } |
|
474 | |||
475 | 4 | if (!isset($this->array_data[0][$key])) { |
|
476 | return null; |
||
477 | 4 | } |
|
478 | 4 | ||
479 | 4 | return $this->array_data[0][$key]; |
|
480 | 4 | } |
|
481 | |||
482 | /** |
||
483 | 4 | * @param string|null $path |
|
484 | * @return array |
||
485 | * @throws Exception\TransportException |
||
486 | */ |
||
487 | public function rowsAsTree($path) |
||
488 | { |
||
489 | $this->init(); |
||
490 | |||
491 | $out = []; |
||
492 | foreach ($this->array_data as $row) { |
||
493 | $d = $this->array_to_tree($row, $path); |
||
494 | $out = array_replace_recursive($d, $out); |
||
495 | } |
||
496 | |||
497 | return $out; |
||
498 | } |
||
499 | |||
500 | /** |
||
501 | * Return size_upload,upload_content,speed_upload,time_request |
||
502 | * |
||
503 | * @return array |
||
504 | * @throws Exception\TransportException |
||
505 | */ |
||
506 | public function info_upload() |
||
507 | { |
||
508 | $this->check(); |
||
509 | 1 | return [ |
|
510 | 'size_upload' => $this->response()->size_upload(), |
||
511 | 1 | 'upload_content' => $this->response()->upload_content_length(), |
|
512 | 'speed_upload' => $this->response()->speed_upload(), |
||
513 | 1 | 'time_request' => $this->response()->total_time() |
|
514 | 1 | ]; |
|
515 | 1 | } |
|
516 | 1 | ||
517 | 1 | /** |
|
518 | 1 | * Return size_upload,upload_content,speed_upload,time_request,starttransfer_time,size_download,speed_download |
|
519 | 1 | * |
|
520 | * @return array |
||
521 | * @throws Exception\TransportException |
||
522 | */ |
||
523 | public function info() |
||
524 | { |
||
525 | $this->check(); |
||
526 | return [ |
||
527 | 1 | 'starttransfer_time' => $this->response()->starttransfer_time(), |
|
528 | 'size_download' => $this->response()->size_download(), |
||
529 | 1 | 'speed_download' => $this->response()->speed_download(), |
|
530 | 'size_upload' => $this->response()->size_upload(), |
||
531 | 'upload_content' => $this->response()->upload_content_length(), |
||
532 | 'speed_upload' => $this->response()->speed_upload(), |
||
533 | 'time_request' => $this->response()->total_time() |
||
534 | ]; |
||
535 | } |
||
536 | 9 | ||
537 | /** |
||
538 | 9 | * get format in sql |
|
539 | 8 | * @return mixed |
|
540 | */ |
||
541 | public function getFormat() |
||
542 | { |
||
543 | return $this->format; |
||
544 | } |
||
545 | |||
546 | /** |
||
547 | 4 | * @return array |
|
548 | * @throws Exception\TransportException |
||
549 | 4 | */ |
|
550 | public function rows() |
||
551 | { |
||
552 | 4 | $this->init(); |
|
553 | 4 | return $this->array_data; |
|
554 | } |
||
555 | 4 | ||
556 | 4 | /** |
|
557 | 4 | * @return false|string |
|
558 | */ |
||
559 | public function jsonRows() |
||
560 | { |
||
561 | return json_encode($this->rows(), JSON_PRETTY_PRINT); |
||
562 | } |
||
563 | |||
564 | 4 | /** |
|
565 | 4 | * @param array|string $arr |
|
566 | 4 | * @param null|string|array $path |
|
567 | * @return array |
||
568 | 4 | */ |
|
569 | 4 | private function array_to_tree($arr, $path = null) |
|
570 | { |
||
571 | if (is_array($path)) { |
||
572 | $keys = $path; |
||
573 | } else { |
||
574 | 4 | $args = func_get_args(); |
|
575 | array_shift($args); |
||
576 | 4 | ||
577 | if (sizeof($args) < 2) { |
||
578 | $separator = '.'; |
||
579 | 4 | $keys = explode($separator, $path); |
|
580 | } else { |
||
581 | $keys = $args; |
||
582 | } |
||
583 | } |
||
584 | |||
585 | // |
||
586 | $tree = $arr; |
||
587 | while (count($keys)) { |
||
588 | $key = array_pop($keys); |
||
589 | |||
590 | if (isset($arr[$key])) { |
||
591 | $val = $arr[$key]; |
||
592 | } else { |
||
593 | $val = $key; |
||
594 | } |
||
595 | |||
596 | $tree = array($val => $tree); |
||
597 | } |
||
598 | if (!is_array($tree)) { |
||
599 | return []; |
||
600 | } |
||
601 | return $tree; |
||
602 | } |
||
603 | |||
604 | |||
605 | public function rewind(): void { |
||
606 | $this->iterator = 0; |
||
607 | } |
||
608 | |||
609 | /** |
||
610 | * @return mixed |
||
611 | */ |
||
612 | #[\ReturnTypeWillChange] |
||
613 | public function current() { |
||
614 | if (!isset($this->array_data[$this->iterator])) { |
||
615 | return null; |
||
616 | } |
||
617 | return $this->array_data[$this->iterator]; |
||
618 | } |
||
619 | |||
620 | public function key(): int { |
||
621 | return $this->iterator; |
||
622 | } |
||
623 | |||
624 | public function next(): void { |
||
625 | ++$this->iterator; |
||
626 | } |
||
627 | |||
628 | public function valid(): bool { |
||
629 | $this->init(); |
||
630 | return isset($this->array_data[$this->iterator]); |
||
631 | } |
||
632 | } |
||
633 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.