1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ClickHouseDB; |
4
|
|
|
|
5
|
|
|
use ClickHouseDB\Exception\DatabaseException; |
6
|
|
|
use ClickHouseDB\Exception\QueryException; |
7
|
|
|
use ClickHouseDB\Query\Query; |
8
|
|
|
use ClickHouseDB\Transport\CurlerRequest; |
9
|
|
|
use ClickHouseDB\Transport\CurlerResponse; |
10
|
|
|
|
11
|
|
|
class Statement |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* @var string |
15
|
|
|
*/ |
16
|
|
|
private $_rawData; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @var int |
20
|
|
|
*/ |
21
|
|
|
private $_http_code = -1; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var CurlerRequest |
25
|
|
|
*/ |
26
|
|
|
private $_request = null; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var bool |
30
|
|
|
*/ |
31
|
|
|
private $_init = false; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var Query |
35
|
|
|
*/ |
36
|
|
|
private $query; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var mixed |
40
|
|
|
*/ |
41
|
|
|
private $format; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
private $sql = ''; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
private $meta; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
private $data; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var array |
60
|
|
|
*/ |
61
|
|
|
private $totals; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
private $extremes; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @var int |
70
|
|
|
*/ |
71
|
|
|
private $rows; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var bool|integer |
75
|
|
|
*/ |
76
|
|
|
private $rows_before_limit_at_least = false; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
private $array_data = []; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @var array |
85
|
|
|
*/ |
86
|
|
|
private $statistics; |
87
|
|
|
|
88
|
|
|
|
89
|
35 |
|
public function __construct(CurlerRequest $request) |
90
|
|
|
{ |
91
|
35 |
|
$this->_request = $request; |
92
|
35 |
|
$this->format = $this->_request->getRequestExtendedInfo('format'); |
93
|
35 |
|
$this->query = $this->_request->getRequestExtendedInfo('query'); |
94
|
35 |
|
$this->sql = $this->_request->getRequestExtendedInfo('sql'); |
95
|
35 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @return CurlerRequest |
99
|
|
|
*/ |
100
|
|
|
public function getRequest() |
101
|
|
|
{ |
102
|
|
|
return $this->_request; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @return CurlerResponse |
107
|
|
|
* @throws Exception\TransportException |
108
|
|
|
*/ |
109
|
34 |
|
private function response() |
110
|
|
|
{ |
111
|
34 |
|
return $this->_request->response(); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @return mixed |
116
|
|
|
* @throws Exception\TransportException |
117
|
|
|
*/ |
118
|
1 |
|
public function responseInfo() |
119
|
|
|
{ |
120
|
1 |
|
return $this->response()->info(); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @return mixed|string |
125
|
|
|
*/ |
126
|
9 |
|
public function sql() |
127
|
|
|
{ |
128
|
9 |
|
return $this->sql; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @param string $body |
133
|
|
|
* @return array|bool |
134
|
|
|
*/ |
135
|
5 |
|
private function parseErrorClickHouse($body) |
136
|
|
|
{ |
137
|
5 |
|
$body = trim($body); |
138
|
5 |
|
$mathes = []; |
139
|
|
|
|
140
|
|
|
// Code: 115, e.displayText() = DB::Exception: Unknown setting readonly[0], e.what() = DB::Exception |
141
|
|
|
// Code: 192, e.displayText() = DB::Exception: Unknown user x, e.what() = DB::Exception |
142
|
|
|
// Code: 60, e.displayText() = DB::Exception: Table default.ZZZZZ doesn't exist., e.what() = DB::Exception |
143
|
|
|
|
144
|
5 |
|
if (preg_match("%Code: (\d+),\se\.displayText\(\) \=\s*DB\:\:Exception\s*:\s*(.*)\,\s*e\.what.*%ius", $body, $mathes)) { |
145
|
5 |
|
return ['code' => $mathes[1], 'message' => $mathes[2]]; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return false; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @return bool |
153
|
|
|
* @throws Exception\TransportException |
154
|
|
|
*/ |
155
|
10 |
|
public function error() |
156
|
|
|
{ |
157
|
10 |
|
if (!$this->isError()) { |
158
|
2 |
|
return false; |
159
|
|
|
} |
160
|
|
|
|
161
|
8 |
|
$body = $this->response()->body(); |
162
|
8 |
|
$error_no = $this->response()->error_no(); |
163
|
8 |
|
$error=$this->response()->error(); |
164
|
|
|
|
165
|
8 |
|
if (!$error_no && !$error) { |
166
|
5 |
|
$parse = $this->parseErrorClickHouse($body); |
167
|
|
|
|
168
|
5 |
|
if ($parse) { |
169
|
5 |
|
throw new DatabaseException($parse['message'] . "\nIN:" . $this->sql(), $parse['code']); |
170
|
|
|
} |
171
|
|
|
else { |
172
|
|
|
$code = $this->response()->http_code(); |
173
|
|
|
$message = "HttpCode:" . $this->response()->http_code() . " ; ".$this->response()->error()." ;" . $body; |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
else { |
177
|
3 |
|
$code = $error_no; |
178
|
3 |
|
$message = $this->response()->error(); |
179
|
|
|
} |
180
|
|
|
|
181
|
3 |
|
throw new QueryException($message, $code); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @return bool |
186
|
|
|
* @throws Exception\TransportException |
187
|
|
|
*/ |
188
|
34 |
|
public function isError() |
189
|
|
|
{ |
190
|
34 |
|
return ($this->response()->http_code() !== 200 || $this->response()->error_no()); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @return bool |
195
|
|
|
* @throws Exception\TransportException |
196
|
|
|
*/ |
197
|
34 |
|
private function check() |
198
|
|
|
{ |
199
|
34 |
|
if (!$this->_request->isResponseExists()) { |
200
|
|
|
throw new QueryException('Not have response'); |
201
|
|
|
} |
202
|
|
|
|
203
|
34 |
|
if ($this->isError()) { |
204
|
3 |
|
$this->error(); |
205
|
|
|
} |
206
|
|
|
|
207
|
34 |
|
return true; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @return bool |
212
|
|
|
* @throws Exception\TransportException |
213
|
|
|
*/ |
214
|
34 |
|
private function init() |
215
|
|
|
{ |
216
|
34 |
|
if ($this->_init) { |
217
|
|
|
return false; |
218
|
|
|
} |
219
|
|
|
|
220
|
34 |
|
$this->check(); |
221
|
|
|
|
222
|
|
|
|
223
|
34 |
|
$this->_rawData = $this->response()->rawDataOrJson($this->format); |
|
|
|
|
224
|
|
|
|
225
|
34 |
|
if (!$this->_rawData) { |
226
|
|
|
$this->_init = true; |
227
|
|
|
return false; |
228
|
|
|
} |
229
|
|
|
|
230
|
34 |
|
foreach (['meta', 'data', 'totals', 'extremes', 'rows', 'rows_before_limit_at_least','statistics'] as $key) { |
231
|
34 |
|
if (isset($this->_rawData[$key])) { |
232
|
34 |
|
$this->{$key} = $this->_rawData[$key]; |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
34 |
|
if (empty($this->meta)) { |
237
|
|
|
throw new QueryException('Can`t find meta'); |
238
|
|
|
} |
239
|
|
|
|
240
|
34 |
|
$this->array_data = []; |
241
|
34 |
|
foreach ($this->data as $rows) { |
242
|
34 |
|
$r = []; |
243
|
|
|
|
244
|
34 |
|
foreach ($this->meta as $meta) { |
245
|
34 |
|
$r[$meta['name']] = $rows[$meta['name']]; |
246
|
|
|
} |
247
|
|
|
|
248
|
34 |
|
$this->array_data[] = $r; |
249
|
|
|
} |
250
|
|
|
|
251
|
34 |
|
return true; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* @return array |
256
|
|
|
* @throws \Exception |
257
|
|
|
*/ |
258
|
|
|
public function extremes() |
259
|
|
|
{ |
260
|
|
|
$this->init(); |
261
|
|
|
return $this->extremes; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @return mixed |
266
|
|
|
* @throws Exception\TransportException |
267
|
|
|
*/ |
268
|
1 |
|
public function totalTimeRequest() |
269
|
|
|
{ |
270
|
1 |
|
$this->check(); |
271
|
1 |
|
return $this->response()->total_time(); |
272
|
|
|
|
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @return array |
277
|
|
|
* @throws \Exception |
278
|
|
|
*/ |
279
|
1 |
|
public function extremesMin() |
280
|
|
|
{ |
281
|
1 |
|
$this->init(); |
282
|
|
|
|
283
|
1 |
|
if (empty($this->extremes['min'])) { |
284
|
|
|
return []; |
285
|
|
|
} |
286
|
|
|
|
287
|
1 |
|
return $this->extremes['min']; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* @return array |
292
|
|
|
* @throws \Exception |
293
|
|
|
*/ |
294
|
|
|
public function extremesMax() |
295
|
|
|
{ |
296
|
|
|
$this->init(); |
297
|
|
|
|
298
|
|
|
if (empty($this->extremes['max'])) { |
299
|
|
|
return []; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
return $this->extremes['max']; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* @return array |
307
|
|
|
* @throws Exception\TransportException |
308
|
|
|
*/ |
309
|
1 |
|
public function totals() |
310
|
|
|
{ |
311
|
1 |
|
$this->init(); |
312
|
1 |
|
return $this->totals; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* |
317
|
|
|
*/ |
318
|
|
|
public function dumpRaw() |
319
|
|
|
{ |
320
|
|
|
print_r($this->_rawData); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* |
325
|
|
|
*/ |
326
|
|
|
public function dump() |
327
|
|
|
{ |
328
|
|
|
$this->_request->dump(); |
329
|
|
|
$this->response()->dump(); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @return bool|int |
334
|
|
|
* @throws Exception\TransportException |
335
|
|
|
*/ |
336
|
2 |
|
public function countAll() |
337
|
|
|
{ |
338
|
2 |
|
$this->init(); |
339
|
2 |
|
return $this->rows_before_limit_at_least; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* @param bool $key |
344
|
|
|
* @return array|mixed|null |
345
|
|
|
* @throws Exception\TransportException |
346
|
|
|
*/ |
347
|
|
|
public function statistics($key=false) |
348
|
|
|
{ |
349
|
|
|
$this->init(); |
350
|
|
|
if ($key) |
351
|
|
|
{ |
352
|
|
|
if (!is_array($this->statistics)) return null; |
|
|
|
|
353
|
|
|
if (!isset($this->statistics[$key])) return null; |
354
|
|
|
return $this->statistics[$key]; |
355
|
|
|
} |
356
|
|
|
return $this->statistics; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @return int |
361
|
|
|
* @throws Exception\TransportException |
362
|
|
|
*/ |
363
|
6 |
|
public function count() |
364
|
|
|
{ |
365
|
6 |
|
$this->init(); |
366
|
6 |
|
return $this->rows; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @return mixed|string |
371
|
|
|
* @throws Exception\TransportException |
372
|
|
|
*/ |
373
|
2 |
|
public function rawData() |
374
|
|
|
{ |
375
|
2 |
|
if ($this->_init) { |
376
|
|
|
return $this->_rawData; |
377
|
|
|
} |
378
|
|
|
|
379
|
2 |
|
$this->check(); |
380
|
|
|
|
381
|
2 |
|
return $this->response()->rawDataOrJson($this->format); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* @param string $key |
386
|
|
|
* @return mixed|null |
387
|
|
|
* @throws Exception\TransportException |
388
|
|
|
*/ |
389
|
33 |
|
public function fetchOne($key = '') |
390
|
|
|
{ |
391
|
33 |
|
$this->init(); |
392
|
|
|
|
393
|
33 |
|
if (isset($this->array_data[0])) { |
394
|
33 |
|
if ($key) { |
395
|
33 |
|
if (isset($this->array_data[0][$key])) { |
396
|
33 |
|
return $this->array_data[0][$key]; |
397
|
|
|
} |
398
|
|
|
else { |
399
|
|
|
return null; |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
2 |
|
return $this->array_data[0]; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
return null; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @param string|null $path |
411
|
|
|
* @return array |
412
|
|
|
* @throws Exception\TransportException |
413
|
|
|
*/ |
414
|
3 |
|
public function rowsAsTree($path) |
415
|
|
|
{ |
416
|
3 |
|
$this->init(); |
417
|
|
|
|
418
|
3 |
|
$out = []; |
419
|
3 |
|
foreach ($this->array_data as $row) { |
420
|
3 |
|
$d = $this->array_to_tree($row, $path); |
421
|
3 |
|
$out = array_replace_recursive($d, $out); |
422
|
|
|
} |
423
|
|
|
|
424
|
3 |
|
return $out; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Return size_upload,upload_content,speed_upload,time_request |
429
|
|
|
* |
430
|
|
|
* @return array |
431
|
|
|
* @throws Exception\TransportException |
432
|
|
|
*/ |
433
|
|
|
public function info_upload() |
434
|
|
|
{ |
435
|
|
|
$this->check(); |
436
|
|
|
return [ |
437
|
|
|
'size_upload' => $this->response()->size_upload(), |
438
|
|
|
'upload_content' => $this->response()->upload_content_length(), |
439
|
|
|
'speed_upload' => $this->response()->speed_upload(), |
440
|
|
|
'time_request' => $this->response()->total_time() |
441
|
|
|
]; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Return size_upload,upload_content,speed_upload,time_request,starttransfer_time,size_download,speed_download |
446
|
|
|
* |
447
|
|
|
* @return array |
448
|
|
|
* @throws Exception\TransportException |
449
|
|
|
*/ |
450
|
1 |
|
public function info() |
451
|
|
|
{ |
452
|
1 |
|
$this->check(); |
453
|
|
|
return [ |
454
|
1 |
|
'starttransfer_time' => $this->response()->starttransfer_time(), |
455
|
1 |
|
'size_download' => $this->response()->size_download(), |
456
|
1 |
|
'speed_download' => $this->response()->speed_download(), |
457
|
1 |
|
'size_upload' => $this->response()->size_upload(), |
458
|
1 |
|
'upload_content' => $this->response()->upload_content_length(), |
459
|
1 |
|
'speed_upload' => $this->response()->speed_upload(), |
460
|
1 |
|
'time_request' => $this->response()->total_time() |
461
|
|
|
]; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* get format in sql |
466
|
|
|
* @return mixed |
467
|
|
|
*/ |
468
|
1 |
|
public function getFormat() |
469
|
|
|
{ |
470
|
1 |
|
return $this->format; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* @return array |
475
|
|
|
* @throws Exception\TransportException |
476
|
|
|
*/ |
477
|
3 |
|
public function rows() |
478
|
|
|
{ |
479
|
3 |
|
$this->init(); |
480
|
2 |
|
return $this->array_data; |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* @param array|string $arr |
485
|
|
|
* @param null|string|array $path |
486
|
|
|
* @return array |
487
|
|
|
*/ |
488
|
3 |
|
private function array_to_tree($arr, $path = null) |
489
|
|
|
{ |
490
|
3 |
|
if (is_array($path)) { |
491
|
|
|
$keys = $path; |
492
|
|
|
} |
493
|
|
|
else { |
494
|
3 |
|
$args = func_get_args(); |
495
|
3 |
|
array_shift($args); |
496
|
|
|
|
497
|
3 |
|
if (sizeof($args) < 2) { |
498
|
3 |
|
$separator = '.'; |
499
|
3 |
|
$keys = explode($separator, $path); |
500
|
|
|
} |
501
|
|
|
else { |
502
|
|
|
$keys = $args; |
503
|
|
|
} |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
// |
507
|
3 |
|
$tree = $arr; |
508
|
3 |
|
while (count($keys)) { |
509
|
3 |
|
$key = array_pop($keys); |
510
|
|
|
|
511
|
3 |
|
if (isset($arr[$key])) { |
512
|
3 |
|
$val = $arr[$key]; |
513
|
|
|
} |
514
|
|
|
else { |
515
|
|
|
$val = $key; |
516
|
|
|
} |
517
|
|
|
|
518
|
3 |
|
$tree = array($val => $tree); |
519
|
|
|
} |
520
|
3 |
|
if (!is_array($tree)) return []; |
521
|
3 |
|
return $tree; |
522
|
|
|
} |
523
|
|
|
} |
524
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.