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"; |
||
0 ignored issues
–
show
|
|||
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) { |
|
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; |
|
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
The string literal
HttpCode: does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() Coding Style
Comprehensibility
introduced
by
The string literal
; does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() Coding Style
Comprehensibility
introduced
by
The string literal
; does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. ![]() |
|||
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 |
PHP provides two ways to mark string literals. Either with single quotes
'literal'
or with double quotes"literal"
. The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (
\'
) and the backslash (\\
). Every other character is displayed as is.Double quoted string literals may contain other variables or more complex escape sequences.
will print an indented:
Single is Value
If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.
For more information on PHP string literals and available escape sequences see the PHP core documentation.