Issues (2354)

src/Statement.php (6 issues)

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
Coding Style Comprehensibility introduced by
The string literal %Code:\s(\d+)\.\s*DB::Ex...\.what|\(version).*%ius 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 '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.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

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.

Loading history...
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 {
0 ignored issues
show
Expected 0 blank lines after opening function brace; 1 found
Loading history...
156 8
157
        return preg_match(self::CLICKHOUSE_ERROR_REGEX, $body) === 1;
0 ignored issues
show
Expected 0 lines before "return", found 1.
Loading history...
Function preg_match() should not be referenced via a fallback global name, but via a use statement.
Loading history...
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;
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())) {
0 ignored issues
show
Useless condition.
Loading history...
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()
0 ignored issues
show
Method \ClickHouseDB\Statement::isInited() does not have native return type hint for its return value but it should be possible to add it based on @return annotation "bool".
Loading history...
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