1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Level23\Druid\Queries; |
5
|
|
|
|
6
|
|
|
use InvalidArgumentException; |
7
|
|
|
use Level23\Druid\DruidClient; |
8
|
|
|
use Level23\Druid\Types\DataType; |
9
|
|
|
use Level23\Druid\Concerns\HasLimit; |
10
|
|
|
use Level23\Druid\Types\Granularity; |
11
|
|
|
use Level23\Druid\Concerns\HasFilter; |
12
|
|
|
use Level23\Druid\Concerns\HasHaving; |
13
|
|
|
use Level23\Druid\Types\SortingOrder; |
14
|
|
|
use Level23\Druid\Dimensions\Dimension; |
15
|
|
|
use Level23\Druid\Context\QueryContext; |
16
|
|
|
use Level23\Druid\Concerns\HasIntervals; |
17
|
|
|
use Level23\Druid\Limits\LimitInterface; |
18
|
|
|
use Level23\Druid\Concerns\HasDimensions; |
19
|
|
|
use Level23\Druid\Types\OrderByDirection; |
20
|
|
|
use Level23\Druid\Concerns\HasDataSource; |
21
|
|
|
use Level23\Druid\Responses\QueryResponse; |
22
|
|
|
use Level23\Druid\Concerns\HasAggregations; |
23
|
|
|
use Level23\Druid\Context\TopNQueryContext; |
24
|
|
|
use Level23\Druid\Context\ScanQueryContext; |
25
|
|
|
use Level23\Druid\Concerns\HasSearchFilters; |
26
|
|
|
use Level23\Druid\Concerns\HasVirtualColumns; |
27
|
|
|
use Level23\Druid\Types\ScanQueryResultFormat; |
28
|
|
|
use Level23\Druid\Responses\ScanQueryResponse; |
29
|
|
|
use Level23\Druid\Responses\TopNQueryResponse; |
30
|
|
|
use Level23\Druid\DataSources\TableDataSource; |
31
|
|
|
use Level23\Druid\Concerns\HasPostAggregations; |
32
|
|
|
use Level23\Druid\Context\GroupByV1QueryContext; |
33
|
|
|
use Level23\Druid\Context\GroupByV2QueryContext; |
34
|
|
|
use Level23\Druid\Responses\SelectQueryResponse; |
35
|
|
|
use Level23\Druid\Responses\SearchQueryResponse; |
36
|
|
|
use Level23\Druid\Collections\IntervalCollection; |
37
|
|
|
use Level23\Druid\Context\TimeSeriesQueryContext; |
38
|
|
|
use Level23\Druid\Responses\GroupByQueryResponse; |
39
|
|
|
use Level23\Druid\Collections\DimensionCollection; |
40
|
|
|
use Level23\Druid\DataSources\DataSourceInterface; |
41
|
|
|
use Level23\Druid\Collections\AggregationCollection; |
42
|
|
|
use Level23\Druid\Responses\TimeSeriesQueryResponse; |
43
|
|
|
use Level23\Druid\Collections\VirtualColumnCollection; |
44
|
|
|
use Level23\Druid\Collections\PostAggregationCollection; |
45
|
|
|
use Level23\Druid\Responses\SegmentMetadataQueryResponse; |
46
|
|
|
use function json_encode; |
47
|
|
|
use function json_last_error; |
48
|
|
|
use function json_last_error_msg; |
49
|
|
|
|
50
|
|
|
class QueryBuilder |
51
|
|
|
{ |
52
|
|
|
use HasFilter, HasHaving, HasDimensions, HasAggregations, HasIntervals, HasLimit, HasVirtualColumns, HasPostAggregations, HasSearchFilters, HasDataSource; |
|
|
|
|
53
|
|
|
|
54
|
|
|
protected DruidClient $client; |
55
|
|
|
|
56
|
|
|
protected ?QueryBuilder $query = null; |
57
|
|
|
|
58
|
|
|
protected DataSourceInterface $dataSource; |
59
|
|
|
|
60
|
|
|
protected string $granularity; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @var array|\Level23\Druid\PostAggregations\PostAggregatorInterface[] |
64
|
|
|
*/ |
65
|
|
|
protected array $postAggregations = []; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Set a paging identifier for a Select query. |
69
|
|
|
* |
70
|
|
|
* @var array<string,int>|null |
71
|
|
|
*/ |
72
|
|
|
protected ?array $pagingIdentifier = null; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* The subtotal spec (only applies for groupBy queries) |
76
|
|
|
* |
77
|
|
|
* @var array<array<string>> |
78
|
|
|
*/ |
79
|
|
|
protected array $subtotals = []; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* The metrics to select when using a Select Query. |
83
|
|
|
* When empty, all metrics are returned. |
84
|
|
|
* |
85
|
|
|
* @var array<string> |
86
|
|
|
*/ |
87
|
|
|
protected array $metrics = []; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* This contains a list of "temporary" field names which we will use to store our result of |
91
|
|
|
* a virtual column when the whereFlag() method is used. |
92
|
|
|
* |
93
|
|
|
* @var array<string> |
94
|
|
|
*/ |
95
|
|
|
public array $placeholders = []; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* QueryBuilder constructor. |
99
|
|
|
* |
100
|
|
|
* @param \Level23\Druid\DruidClient $client |
101
|
|
|
* @param string $dataSource |
102
|
|
|
* @param string $granularity |
103
|
|
|
*/ |
104
|
343 |
|
public function __construct(DruidClient $client, string $dataSource = '', string $granularity = Granularity::ALL) |
105
|
|
|
{ |
106
|
343 |
|
$this->client = $client; |
107
|
343 |
|
$this->query = $this; |
108
|
343 |
|
$this->dataSource = new TableDataSource($dataSource); |
109
|
343 |
|
$this->granularity = Granularity::validate($granularity); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Create a virtual column and select the result. |
114
|
|
|
* |
115
|
|
|
* Virtual columns are queryable column "views" created from a set of columns during a query. |
116
|
|
|
* |
117
|
|
|
* A virtual column can potentially draw from multiple underlying columns, although a virtual column always |
118
|
|
|
* presents itself as a single column. |
119
|
|
|
* |
120
|
|
|
* @param string $expression |
121
|
|
|
* @param string $as |
122
|
|
|
* @param string $outputType |
123
|
|
|
* |
124
|
|
|
* @return $this |
125
|
|
|
* @see https://druid.apache.org/docs/latest/misc/math-expr.html |
126
|
|
|
*/ |
127
|
1 |
|
public function selectVirtual(string $expression, string $as, string $outputType = DataType::STRING): self |
128
|
|
|
{ |
129
|
1 |
|
$this->virtualColumn($expression, $as, $outputType); |
130
|
1 |
|
$this->select($as, $as, null, $outputType); |
|
|
|
|
131
|
|
|
|
132
|
1 |
|
return $this; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Execute a druid query. We will try to detect the best possible query type possible. |
137
|
|
|
* |
138
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
139
|
|
|
* |
140
|
|
|
* @return QueryResponse |
141
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
142
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
143
|
|
|
*/ |
144
|
1 |
|
public function execute($context = []): QueryResponse |
145
|
|
|
{ |
146
|
1 |
|
$query = $this->getQuery($context); |
147
|
|
|
|
148
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
149
|
|
|
|
150
|
1 |
|
return $query->parseResponse($rawResponse); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Update/set the granularity |
155
|
|
|
* |
156
|
|
|
* @param string $granularity |
157
|
|
|
* |
158
|
|
|
* @return $this |
159
|
|
|
*/ |
160
|
17 |
|
public function granularity(string $granularity): QueryBuilder |
161
|
|
|
{ |
162
|
17 |
|
$this->granularity = Granularity::validate($granularity); |
163
|
|
|
|
164
|
17 |
|
return $this; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Define an array which should contain arrays with dimensions where you want to retrieve the subtotals for. |
169
|
|
|
* NOTE: This only applies for groupBy queries. |
170
|
|
|
* |
171
|
|
|
* This is like doing a WITH ROLLUP in an SQL query. |
172
|
|
|
* |
173
|
|
|
* Example: Imagine that you count the number of people. You want to do this for per city, but also |
174
|
|
|
* per province, per country and per continent. This method allows you to do that all at once. Druid will |
175
|
|
|
* do the "sum(people)" per subtotal row. |
176
|
|
|
* |
177
|
|
|
* Example: |
178
|
|
|
* Array( |
179
|
|
|
* Array('continent', 'country', 'province', 'city'), |
180
|
|
|
* Array('continent', 'country', 'province'), |
181
|
|
|
* Array('continent', 'country'), |
182
|
|
|
* Array('continent'), |
183
|
|
|
* ) |
184
|
|
|
* |
185
|
|
|
* @param array<array<string>> $subtotals |
186
|
|
|
* |
187
|
|
|
* @return $this |
188
|
|
|
*/ |
189
|
3 |
|
public function subtotals(array $subtotals): self |
190
|
|
|
{ |
191
|
3 |
|
$this->subtotals = $subtotals; |
192
|
|
|
|
193
|
3 |
|
return $this; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Select the metrics which should be returned when using a selectQuery. |
198
|
|
|
* If this is not specified, all metrics are returned (which is default). |
199
|
|
|
* |
200
|
|
|
* NOTE: This only applies to select queries! |
201
|
|
|
* |
202
|
|
|
* @param array<string> $metrics |
203
|
|
|
* |
204
|
|
|
* @return $this |
205
|
|
|
*/ |
206
|
1 |
|
public function metrics(array $metrics): self |
207
|
|
|
{ |
208
|
1 |
|
$this->metrics = $metrics; |
209
|
|
|
|
210
|
1 |
|
return $this; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Set a paging identifier. This is only applied for a SELECT query! |
215
|
|
|
* |
216
|
|
|
* @param array<string,int> $pagingIdentifier |
217
|
|
|
* |
218
|
|
|
* @return \Level23\Druid\Queries\QueryBuilder |
219
|
|
|
*/ |
220
|
5 |
|
public function pagingIdentifier(array $pagingIdentifier): QueryBuilder |
221
|
|
|
{ |
222
|
5 |
|
$this->pagingIdentifier = $pagingIdentifier; |
223
|
|
|
|
224
|
5 |
|
return $this; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Do a segment metadata query and return the response |
229
|
|
|
* |
230
|
|
|
* @return SegmentMetadataQueryResponse |
231
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
232
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
233
|
|
|
*/ |
234
|
1 |
|
public function segmentMetadata(): SegmentMetadataQueryResponse |
235
|
|
|
{ |
236
|
1 |
|
$query = new SegmentMetadataQuery($this->dataSource, new IntervalCollection(...$this->intervals)); |
237
|
|
|
|
238
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
239
|
|
|
|
240
|
1 |
|
return $query->parseResponse($rawResponse); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Return the query as a JSON string |
245
|
|
|
* |
246
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
247
|
|
|
* |
248
|
|
|
* @return string |
249
|
|
|
* @throws \InvalidArgumentException if the JSON cannot be encoded. |
250
|
|
|
*/ |
251
|
2 |
|
public function toJson($context = []): string |
252
|
|
|
{ |
253
|
2 |
|
$query = $this->getQuery($context); |
254
|
|
|
|
255
|
2 |
|
$json = json_encode($query->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); |
256
|
1 |
|
if (JSON_ERROR_NONE !== json_last_error()) { |
257
|
|
|
throw new InvalidArgumentException( |
258
|
|
|
'json_encode error: ' . json_last_error_msg() |
259
|
|
|
); |
260
|
|
|
} |
261
|
|
|
|
262
|
1 |
|
return (string)$json; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Return the query as an array |
267
|
|
|
* |
268
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
269
|
|
|
* |
270
|
|
|
* @return array<string,array<mixed>|string|int> |
271
|
|
|
*/ |
272
|
3 |
|
public function toArray($context = []): array |
273
|
|
|
{ |
274
|
3 |
|
return $this->getQuery($context)->toArray(); |
|
|
|
|
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Execute a TimeSeries query. |
279
|
|
|
* |
280
|
|
|
* @param array<string,string|int|bool>|TimeSeriesQueryContext $context |
281
|
|
|
* |
282
|
|
|
* @return TimeSeriesQueryResponse |
283
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
284
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
285
|
|
|
*/ |
286
|
1 |
|
public function timeseries($context = []): TimeSeriesQueryResponse |
287
|
|
|
{ |
288
|
1 |
|
$query = $this->buildTimeSeriesQuery($context); |
289
|
|
|
|
290
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
291
|
|
|
|
292
|
1 |
|
return $query->parseResponse($rawResponse); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Execute a Scan Query. |
297
|
|
|
* |
298
|
|
|
* @param array<string,string|int|bool>|ScanQueryContext $context Query context parameters |
299
|
|
|
* @param int|null $rowBatchSize How many rows buffered before return |
300
|
|
|
* to client. Default is 20480 |
301
|
|
|
* @param bool $legacy Return results consistent with the |
302
|
|
|
* legacy "scan-query" contrib extension. |
303
|
|
|
* Defaults to the value set by |
304
|
|
|
* druid.query.scan.legacy, which in turn |
305
|
|
|
* defaults to false. See Legacy mode for |
306
|
|
|
* details. |
307
|
|
|
* @param string $resultFormat Result Format. Use one of the |
308
|
|
|
* ScanQueryResultFormat::* constants. |
309
|
|
|
* |
310
|
|
|
* @return ScanQueryResponse |
311
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
312
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
313
|
|
|
*/ |
314
|
5 |
|
public function scan( |
315
|
|
|
$context = [], |
316
|
|
|
?int $rowBatchSize = null, |
317
|
|
|
bool $legacy = false, |
318
|
|
|
string $resultFormat = ScanQueryResultFormat::NORMAL_LIST |
319
|
|
|
): ScanQueryResponse { |
320
|
5 |
|
$query = $this->buildScanQuery($context, $rowBatchSize, $legacy, $resultFormat); |
321
|
|
|
|
322
|
2 |
|
$rawResponse = $this->client->executeQuery($query); |
323
|
|
|
|
324
|
2 |
|
return $query->parseResponse($rawResponse); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Execute a select query. |
329
|
|
|
* |
330
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
331
|
|
|
* |
332
|
|
|
* @return SelectQueryResponse |
333
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
334
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
335
|
|
|
*/ |
336
|
3 |
|
public function selectQuery($context = []): SelectQueryResponse |
337
|
|
|
{ |
338
|
3 |
|
$query = $this->buildSelectQuery($context); |
339
|
|
|
|
340
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
341
|
|
|
|
342
|
1 |
|
return $query->parseResponse($rawResponse); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Execute a topN query. |
347
|
|
|
* |
348
|
|
|
* @param array<string,string|int|bool>|TopNQueryContext $context |
349
|
|
|
* |
350
|
|
|
* @return TopNQueryResponse |
351
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
352
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
353
|
|
|
*/ |
354
|
4 |
|
public function topN($context = []): TopNQueryResponse |
355
|
|
|
{ |
356
|
4 |
|
$query = $this->buildTopNQuery($context); |
357
|
|
|
|
358
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
359
|
|
|
|
360
|
1 |
|
return $query->parseResponse($rawResponse); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Return the group by query |
365
|
|
|
* |
366
|
|
|
* @param array<string,string|int|bool>|GroupByV2QueryContext|GroupByV1QueryContext $context |
367
|
|
|
* |
368
|
|
|
* @return GroupByQueryResponse |
369
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
370
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
371
|
|
|
*/ |
372
|
1 |
|
public function groupBy($context = []): GroupByQueryResponse |
373
|
|
|
{ |
374
|
1 |
|
$query = $this->buildGroupByQuery($context); |
375
|
|
|
|
376
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
377
|
|
|
|
378
|
1 |
|
return $query->parseResponse($rawResponse); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Return the group by query |
383
|
|
|
* |
384
|
|
|
* @param array<string,string|int|bool>|GroupByV2QueryContext|GroupByV1QueryContext $context |
385
|
|
|
* |
386
|
|
|
* @return GroupByQueryResponse |
387
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
388
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
389
|
|
|
*/ |
390
|
1 |
|
public function groupByV1($context = []): GroupByQueryResponse |
391
|
|
|
{ |
392
|
1 |
|
$query = $this->buildGroupByQuery($context, 'v1'); |
393
|
|
|
|
394
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
395
|
|
|
|
396
|
1 |
|
return $query->parseResponse($rawResponse); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Execute a search query and return the response |
401
|
|
|
* |
402
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
403
|
|
|
* @param string $sortingOrder |
404
|
|
|
* |
405
|
|
|
* @return \Level23\Druid\Responses\SearchQueryResponse |
406
|
|
|
* @throws \GuzzleHttp\Exception\GuzzleException |
407
|
|
|
* @throws \Level23\Druid\Exceptions\QueryResponseException |
408
|
|
|
*/ |
409
|
3 |
|
public function search($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQueryResponse |
410
|
|
|
{ |
411
|
3 |
|
$query = $this->buildSearchQuery($context, $sortingOrder); |
412
|
|
|
|
413
|
1 |
|
$rawResponse = $this->client->executeQuery($query); |
414
|
|
|
|
415
|
1 |
|
return $query->parseResponse($rawResponse); |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
//<editor-fold desc="Protected methods"> |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* In our previous version we required an `order()` with "__time" for TimeSeries, Scan and Select Queries. |
422
|
|
|
* This method makes sure that we are backwards compatible. |
423
|
|
|
* |
424
|
|
|
* @param string|null $dimension If given, we will also check if this dimension was ordered by. |
425
|
|
|
* |
426
|
|
|
* @return bool|null |
427
|
|
|
*/ |
428
|
11 |
|
protected function legacyIsOrderByDirectionDescending(?string $dimension = null): ?bool |
429
|
|
|
{ |
430
|
11 |
|
if ($this->limit) { |
431
|
7 |
|
$orderBy = $this->limit->getOrderByCollection(); |
432
|
|
|
|
433
|
7 |
|
if ($orderBy->count() > 0) { |
434
|
5 |
|
$orderByItems = $orderBy->toArray(); |
435
|
|
|
/** @var string[] $first */ |
436
|
5 |
|
$first = reset($orderByItems); |
437
|
|
|
|
438
|
5 |
|
if ($first['dimension'] == '__time' || ($dimension && $dimension == $first['dimension'])) { |
439
|
4 |
|
return $first['direction'] == OrderByDirection::DESC; |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
444
|
7 |
|
return null; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Build a search query. |
449
|
|
|
* |
450
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
451
|
|
|
* @param string $sortingOrder |
452
|
|
|
* |
453
|
|
|
* @return \Level23\Druid\Queries\SearchQuery |
454
|
|
|
*/ |
455
|
5 |
|
protected function buildSearchQuery($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQuery |
456
|
|
|
{ |
457
|
5 |
|
if (count($this->intervals) == 0) { |
458
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
459
|
|
|
} |
460
|
|
|
|
461
|
4 |
|
if (!$this->searchFilter) { |
462
|
1 |
|
throw new InvalidArgumentException('You have to specify a search filter!'); |
463
|
|
|
} |
464
|
|
|
|
465
|
3 |
|
$query = new SearchQuery( |
466
|
3 |
|
$this->dataSource, |
467
|
3 |
|
$this->granularity, |
468
|
3 |
|
new IntervalCollection(...$this->intervals), |
469
|
3 |
|
$this->searchFilter |
470
|
3 |
|
); |
471
|
|
|
|
472
|
3 |
|
if (count($this->searchDimensions) > 0) { |
473
|
1 |
|
$query->setDimensions($this->searchDimensions); |
474
|
|
|
} |
475
|
|
|
|
476
|
3 |
|
if (is_array($context) && count($context) > 0) { |
477
|
1 |
|
$query->setContext(new QueryContext($context)); |
478
|
2 |
|
} elseif ($context instanceof QueryContext) { |
479
|
1 |
|
$query->setContext($context); |
480
|
|
|
} |
481
|
|
|
|
482
|
3 |
|
if ($this->filter) { |
483
|
2 |
|
$query->setFilter($this->filter); |
484
|
|
|
} |
485
|
|
|
|
486
|
3 |
|
if ($sortingOrder) { |
487
|
3 |
|
$query->setSort($sortingOrder); |
488
|
|
|
} |
489
|
|
|
|
490
|
3 |
|
if ($this->limit && $this->limit->getLimit() !== null) { |
491
|
2 |
|
$query->setLimit($this->limit->getLimit()); |
492
|
|
|
} |
493
|
|
|
|
494
|
3 |
|
return $query; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Build a select query. |
499
|
|
|
* |
500
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
501
|
|
|
* |
502
|
|
|
* @return \Level23\Druid\Queries\SelectQuery |
503
|
|
|
*/ |
504
|
6 |
|
protected function buildSelectQuery($context = []): SelectQuery |
505
|
|
|
{ |
506
|
6 |
|
if (count($this->intervals) == 0) { |
507
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
508
|
|
|
} |
509
|
|
|
|
510
|
5 |
|
if (!$this->limit || $this->limit->getLimit() === null) { |
511
|
1 |
|
throw new InvalidArgumentException('You have to supply a limit'); |
512
|
|
|
} |
513
|
|
|
|
514
|
4 |
|
$limit = $this->limit->getLimit(); |
515
|
|
|
|
516
|
4 |
|
$descending = false; |
517
|
4 |
|
if ($this->direction) { |
518
|
1 |
|
$descending = ($this->direction === OrderByDirection::DESC); |
519
|
3 |
|
} elseif ($this->legacyIsOrderByDirectionDescending() === true) { |
520
|
1 |
|
$descending = true; |
521
|
|
|
} |
522
|
|
|
|
523
|
4 |
|
$query = new SelectQuery( |
524
|
4 |
|
$this->dataSource, |
525
|
4 |
|
new IntervalCollection(...$this->intervals), |
526
|
4 |
|
$limit, |
527
|
4 |
|
count($this->dimensions) > 0 ? new DimensionCollection(...$this->dimensions) : null, |
528
|
4 |
|
$this->metrics, |
529
|
4 |
|
$descending |
530
|
4 |
|
); |
531
|
|
|
|
532
|
4 |
|
if ($this->pagingIdentifier) { |
533
|
2 |
|
$query->setPagingIdentifier($this->pagingIdentifier); |
534
|
|
|
} |
535
|
|
|
|
536
|
4 |
|
if (is_array($context) && count($context) > 0) { |
537
|
1 |
|
$query->setContext(new QueryContext($context)); |
538
|
3 |
|
} elseif ($context instanceof QueryContext) { |
539
|
2 |
|
$query->setContext($context); |
540
|
|
|
} |
541
|
|
|
|
542
|
4 |
|
return $query; |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* Build a scan query. |
547
|
|
|
* |
548
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
549
|
|
|
* @param int|null $rowBatchSize |
550
|
|
|
* @param bool $legacy |
551
|
|
|
* @param string $resultFormat |
552
|
|
|
* |
553
|
|
|
* @return \Level23\Druid\Queries\ScanQuery |
554
|
|
|
*/ |
555
|
12 |
|
protected function buildScanQuery( |
556
|
|
|
$context = [], |
557
|
|
|
?int $rowBatchSize = null, |
558
|
|
|
bool $legacy = false, |
559
|
|
|
string $resultFormat = ScanQueryResultFormat::NORMAL_LIST |
560
|
|
|
): ScanQuery { |
561
|
12 |
|
if (count($this->intervals) == 0) { |
562
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
563
|
|
|
} |
564
|
|
|
|
565
|
11 |
|
if (!$this->isDimensionsListScanCompliant()) { |
566
|
1 |
|
throw new InvalidArgumentException( |
567
|
1 |
|
'Only simple dimension or metric selects are available in a scan query. ' . |
568
|
1 |
|
'Aliases, extractions or lookups are not available.' |
569
|
1 |
|
); |
570
|
|
|
} |
571
|
|
|
|
572
|
10 |
|
$query = new ScanQuery( |
573
|
10 |
|
$this->dataSource, |
574
|
10 |
|
new IntervalCollection(...$this->intervals) |
575
|
10 |
|
); |
576
|
|
|
|
577
|
10 |
|
$columns = []; |
578
|
10 |
|
foreach ($this->dimensions as $dimension) { |
579
|
3 |
|
$columns[] = $dimension->getDimension(); |
580
|
|
|
} |
581
|
|
|
|
582
|
10 |
|
if ($this->direction) { |
583
|
2 |
|
$query->setOrder($this->direction); |
584
|
|
|
} else { |
585
|
8 |
|
$isDescending = $this->legacyIsOrderByDirectionDescending(); |
586
|
8 |
|
if ($isDescending !== null) { |
587
|
2 |
|
$query->setOrder($isDescending ? OrderByDirection::DESC : OrderByDirection::ASC); |
588
|
|
|
} |
589
|
|
|
} |
590
|
|
|
|
591
|
10 |
|
if (count($columns) > 0) { |
592
|
3 |
|
$query->setColumns($columns); |
593
|
|
|
} |
594
|
|
|
|
595
|
10 |
|
if (count($this->virtualColumns) > 0) { |
596
|
1 |
|
$query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns)); |
597
|
|
|
} |
598
|
|
|
|
599
|
10 |
|
if ($this->filter) { |
600
|
5 |
|
$query->setFilter($this->filter); |
601
|
|
|
} |
602
|
|
|
|
603
|
10 |
|
if ($this->limit && $this->limit->getLimit() !== null) { |
604
|
5 |
|
$query->setLimit($this->limit->getLimit()); |
605
|
|
|
} |
606
|
|
|
|
607
|
10 |
|
if ($this->limit && $this->limit->getOffset() !== null) { |
608
|
3 |
|
$query->setOffset($this->limit->getOffset()); |
609
|
|
|
} |
610
|
|
|
|
611
|
10 |
|
if (is_array($context) && count($context) > 0) { |
612
|
1 |
|
$query->setContext(new ScanQueryContext($context)); |
613
|
9 |
|
} elseif ($context instanceof QueryContext) { |
614
|
2 |
|
$query->setContext($context); |
615
|
|
|
} |
616
|
|
|
|
617
|
10 |
|
if ($resultFormat) { |
618
|
10 |
|
$query->setResultFormat($resultFormat); |
619
|
|
|
} |
620
|
|
|
|
621
|
9 |
|
if ($rowBatchSize !== null && $rowBatchSize > 0) { |
622
|
3 |
|
$query->setBatchSize($rowBatchSize); |
623
|
|
|
} |
624
|
|
|
|
625
|
9 |
|
$query->setLegacy($legacy); |
626
|
|
|
|
627
|
9 |
|
return $query; |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
/** |
631
|
|
|
* Build a TimeSeries query. |
632
|
|
|
* |
633
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
634
|
|
|
* |
635
|
|
|
* @return TimeSeriesQuery |
636
|
|
|
*/ |
637
|
8 |
|
protected function buildTimeSeriesQuery($context = []): TimeSeriesQuery |
638
|
|
|
{ |
639
|
8 |
|
if (count($this->intervals) == 0) { |
640
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
641
|
|
|
} |
642
|
|
|
|
643
|
7 |
|
$query = new TimeSeriesQuery( |
644
|
7 |
|
$this->dataSource, |
645
|
7 |
|
new IntervalCollection(...$this->intervals), |
646
|
7 |
|
$this->granularity |
647
|
7 |
|
); |
648
|
|
|
|
649
|
|
|
// check if we want to use a different output name for the __time column |
650
|
7 |
|
$dimension = null; |
651
|
7 |
|
if (count($this->dimensions) == 1) { |
652
|
7 |
|
$dimension = $this->dimensions[0]; |
653
|
|
|
// did we only retrieve the time dimension? |
654
|
7 |
|
if ($dimension->getDimension() == '__time' && $dimension->getOutputName() != '__time') { |
655
|
6 |
|
$query->setTimeOutputName($dimension->getOutputName()); |
656
|
|
|
} |
657
|
|
|
} |
658
|
|
|
|
659
|
7 |
|
if (is_array($context) && count($context) > 0) { |
660
|
3 |
|
$query->setContext(new TimeSeriesQueryContext($context)); |
661
|
4 |
|
} elseif ($context instanceof QueryContext) { |
662
|
4 |
|
$query->setContext($context); |
663
|
|
|
} |
664
|
|
|
|
665
|
7 |
|
if ($this->filter) { |
666
|
1 |
|
$query->setFilter($this->filter); |
667
|
|
|
} |
668
|
|
|
|
669
|
7 |
|
if (count($this->aggregations) > 0) { |
670
|
1 |
|
$query->setAggregations(new AggregationCollection(...$this->aggregations)); |
671
|
|
|
} |
672
|
|
|
|
673
|
7 |
|
if (count($this->postAggregations) > 0) { |
674
|
2 |
|
$query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations)); |
675
|
|
|
} |
676
|
|
|
|
677
|
7 |
|
if (count($this->virtualColumns) > 0) { |
678
|
2 |
|
$query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns)); |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
// If there is a limit set, then apply this on the time series query. |
682
|
7 |
|
if ($this->limit && $this->limit->getLimit() !== null) { |
683
|
6 |
|
$query->setLimit($this->limit->getLimit()); |
684
|
|
|
} |
685
|
|
|
|
686
|
7 |
|
$descending = false; |
687
|
7 |
|
if ($this->direction) { |
688
|
5 |
|
$descending = ($this->direction === OrderByDirection::DESC); |
689
|
2 |
|
} elseif ($this->legacyIsOrderByDirectionDescending($dimension ? $dimension->getOutputName() : null) === true) { |
690
|
1 |
|
$descending = true; |
691
|
|
|
} |
692
|
|
|
|
693
|
7 |
|
$query->setDescending($descending); |
694
|
|
|
|
695
|
7 |
|
return $query; |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
/** |
699
|
|
|
* Build a topN query. |
700
|
|
|
* |
701
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
702
|
|
|
* |
703
|
|
|
* @return TopNQuery |
704
|
|
|
*/ |
705
|
6 |
|
protected function buildTopNQuery($context = []): TopNQuery |
706
|
|
|
{ |
707
|
6 |
|
if (count($this->intervals) == 0) { |
708
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
709
|
|
|
} |
710
|
|
|
|
711
|
5 |
|
if (!$this->limit instanceof LimitInterface || $this->limit->getLimit() === null) { |
712
|
1 |
|
throw new InvalidArgumentException( |
713
|
1 |
|
'You should specify a limit to make use of a top query' |
714
|
1 |
|
); |
715
|
|
|
} |
716
|
|
|
|
717
|
4 |
|
$orderByCollection = $this->limit->getOrderByCollection(); |
718
|
4 |
|
if (count($orderByCollection) == 0) { |
719
|
1 |
|
throw new InvalidArgumentException( |
720
|
1 |
|
'You should specify a an order by direction to make use of a top query' |
721
|
1 |
|
); |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
/** |
725
|
|
|
* @var \Level23\Druid\OrderBy\OrderBy $orderBy |
726
|
|
|
*/ |
727
|
3 |
|
$orderBy = $orderByCollection[0]; |
728
|
|
|
|
729
|
3 |
|
$metric = $orderBy->getDimension(); |
730
|
|
|
|
731
|
|
|
/** @var \Level23\Druid\OrderBy\OrderByInterface $orderBy */ |
732
|
3 |
|
$query = new TopNQuery( |
733
|
3 |
|
$this->dataSource, |
734
|
3 |
|
new IntervalCollection(...$this->intervals), |
735
|
3 |
|
$this->dimensions[0], |
736
|
3 |
|
$this->limit->getLimit(), |
737
|
3 |
|
$metric, |
738
|
3 |
|
$this->granularity |
739
|
3 |
|
); |
740
|
|
|
|
741
|
3 |
|
$query->setDescending( |
742
|
3 |
|
($orderBy->getDirection() == OrderByDirection::DESC) |
743
|
3 |
|
); |
744
|
|
|
|
745
|
3 |
|
if (count($this->aggregations) > 0) { |
746
|
2 |
|
$query->setAggregations(new AggregationCollection(...$this->aggregations)); |
747
|
|
|
} |
748
|
|
|
|
749
|
3 |
|
if (count($this->postAggregations) > 0) { |
750
|
2 |
|
$query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations)); |
751
|
|
|
} |
752
|
|
|
|
753
|
3 |
|
if (count($this->virtualColumns) > 0) { |
754
|
2 |
|
$query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns)); |
755
|
|
|
} |
756
|
|
|
|
757
|
3 |
|
if (is_array($context) && count($context) > 0) { |
758
|
1 |
|
$query->setContext(new TopNQueryContext($context)); |
759
|
2 |
|
} elseif ($context instanceof QueryContext) { |
760
|
1 |
|
$query->setContext($context); |
761
|
|
|
} |
762
|
|
|
|
763
|
3 |
|
if ($this->filter) { |
764
|
2 |
|
$query->setFilter($this->filter); |
765
|
|
|
} |
766
|
|
|
|
767
|
3 |
|
return $query; |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
/** |
771
|
|
|
* Build the group by query |
772
|
|
|
* |
773
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
774
|
|
|
* @param string $type |
775
|
|
|
* |
776
|
|
|
* @return GroupByQuery |
777
|
|
|
*/ |
778
|
5 |
|
protected function buildGroupByQuery($context = [], string $type = 'v2'): GroupByQuery |
779
|
|
|
{ |
780
|
5 |
|
if (count($this->intervals) == 0) { |
781
|
1 |
|
throw new InvalidArgumentException('You have to specify at least one interval'); |
782
|
|
|
} |
783
|
|
|
|
784
|
4 |
|
$query = new GroupByQuery( |
785
|
4 |
|
$this->dataSource, |
786
|
4 |
|
new DimensionCollection(...$this->dimensions), |
787
|
4 |
|
new IntervalCollection(...$this->intervals), |
788
|
4 |
|
new AggregationCollection(...$this->aggregations), |
789
|
4 |
|
$this->granularity |
790
|
4 |
|
); |
791
|
|
|
|
792
|
4 |
|
if (is_array($context)) { |
793
|
|
|
switch ($type) { |
794
|
2 |
|
case 'v1': |
795
|
1 |
|
$context = new GroupByV1QueryContext($context); |
796
|
1 |
|
break; |
797
|
|
|
|
798
|
|
|
default: |
799
|
1 |
|
case 'v2': |
800
|
1 |
|
$context = new GroupByV2QueryContext($context); |
801
|
1 |
|
break; |
802
|
|
|
} |
803
|
|
|
} |
804
|
|
|
|
805
|
4 |
|
$query->setContext($context); |
806
|
|
|
|
807
|
4 |
|
if (count($this->postAggregations) > 0) { |
808
|
2 |
|
$query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations)); |
809
|
|
|
} |
810
|
|
|
|
811
|
4 |
|
if ($this->filter) { |
812
|
2 |
|
$query->setFilter($this->filter); |
813
|
|
|
} |
814
|
|
|
|
815
|
4 |
|
if ($this->limit) { |
816
|
2 |
|
$query->setLimit($this->limit); |
817
|
|
|
} |
818
|
|
|
|
819
|
4 |
|
if (count($this->virtualColumns) > 0) { |
820
|
1 |
|
$query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns)); |
821
|
|
|
} |
822
|
|
|
|
823
|
4 |
|
if (count($this->subtotals) > 0) { |
824
|
2 |
|
$query->setSubtotals($this->subtotals); |
825
|
|
|
} |
826
|
|
|
|
827
|
4 |
|
if ($this->having) { |
828
|
2 |
|
$query->setHaving($this->having); |
829
|
|
|
} |
830
|
|
|
|
831
|
4 |
|
return $query; |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
/** |
835
|
|
|
* Return the query automatically detected based on the requested data. |
836
|
|
|
* |
837
|
|
|
* @param array<string,string|int|bool>|QueryContext $context |
838
|
|
|
* |
839
|
|
|
* @return \Level23\Druid\Queries\QueryInterface |
840
|
|
|
*/ |
841
|
9 |
|
public function getQuery($context = []): QueryInterface |
842
|
|
|
{ |
843
|
|
|
// Check if this is a scan query. This is the preferred way to query when there are |
844
|
|
|
// no aggregations done. |
845
|
9 |
|
if ($this->isScanQuery()) { |
846
|
4 |
|
return $this->buildScanQuery($context); |
847
|
|
|
} |
848
|
|
|
|
849
|
|
|
// If we only have "grouped" by __time, then we can use a time series query. |
850
|
|
|
// This is preferred, because it's a lot faster than doing a group by query. |
851
|
5 |
|
if ($this->isTimeSeriesQuery()) { |
852
|
1 |
|
return $this->buildTimeSeriesQuery($context); |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
// Check if we can use a topN query. |
856
|
4 |
|
if ($this->isTopNQuery()) { |
857
|
1 |
|
return $this->buildTopNQuery($context); |
858
|
|
|
} |
859
|
|
|
|
860
|
|
|
// Check if we can use a select query. |
861
|
3 |
|
if ($this->isSelectQuery()) { |
862
|
1 |
|
return $this->buildSelectQuery($context); |
863
|
|
|
} |
864
|
|
|
|
865
|
|
|
// Check if we can use a search query. |
866
|
2 |
|
if ($this->isSearchQuery()) { |
867
|
1 |
|
return $this->buildSearchQuery($context); |
868
|
|
|
} |
869
|
|
|
|
870
|
1 |
|
return $this->buildGroupByQuery($context); |
871
|
|
|
} |
872
|
|
|
|
873
|
|
|
/** |
874
|
|
|
* Determine if the current query is a TimeSeries query |
875
|
|
|
* |
876
|
|
|
* @return bool |
877
|
|
|
*/ |
878
|
4 |
|
protected function isTimeSeriesQuery(): bool |
879
|
|
|
{ |
880
|
4 |
|
if (count($this->dimensions) != 1) { |
881
|
1 |
|
return false; |
882
|
|
|
} |
883
|
|
|
|
884
|
3 |
|
return $this->dimensions[0]->getDimension() == '__time' |
885
|
3 |
|
&& $this->dimensions[0] instanceof Dimension |
886
|
3 |
|
&& $this->dimensions[0]->getExtractionFunction() === null; |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
/** |
890
|
|
|
* Determine if the current query is topN query |
891
|
|
|
* |
892
|
|
|
* @return bool |
893
|
|
|
*/ |
894
|
4 |
|
protected function isTopNQuery(): bool |
895
|
|
|
{ |
896
|
4 |
|
if (count($this->dimensions) != 1) { |
897
|
1 |
|
return false; |
898
|
|
|
} |
899
|
|
|
|
900
|
3 |
|
return $this->limit |
901
|
3 |
|
&& $this->limit->getLimit() !== null |
902
|
3 |
|
&& $this->limit->getOffset() === null |
903
|
3 |
|
&& count($this->limit->getOrderByCollection()) == 1; |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
/** |
907
|
|
|
* Check if we should use a select query. |
908
|
|
|
* |
909
|
|
|
* @return bool |
910
|
|
|
*/ |
911
|
4 |
|
protected function isSelectQuery(): bool |
912
|
|
|
{ |
913
|
4 |
|
return $this->pagingIdentifier !== null && count($this->aggregations) == 0; |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
/** |
917
|
|
|
* Check if we should use a search query. |
918
|
|
|
* |
919
|
|
|
* @return bool |
920
|
|
|
*/ |
921
|
2 |
|
protected function isSearchQuery(): bool |
922
|
|
|
{ |
923
|
2 |
|
return !empty($this->searchFilter); |
924
|
|
|
} |
925
|
|
|
|
926
|
|
|
/** |
927
|
|
|
* Check if we should use a scan query. |
928
|
|
|
* |
929
|
|
|
* @return bool |
930
|
|
|
*/ |
931
|
7 |
|
protected function isScanQuery(): bool |
932
|
|
|
{ |
933
|
7 |
|
return count($this->aggregations) == 0 && $this->isDimensionsListScanCompliant(); |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
/** |
937
|
|
|
* Return true if the dimensions which are selected can be used as "columns" in a scan query. |
938
|
|
|
* |
939
|
|
|
* @return bool |
940
|
|
|
*/ |
941
|
21 |
|
protected function isDimensionsListScanCompliant(): bool |
942
|
|
|
{ |
943
|
21 |
|
foreach ($this->dimensions as $dimension) { |
944
|
13 |
|
if (!$dimension instanceof Dimension) { |
945
|
5 |
|
return false; |
946
|
|
|
} |
947
|
|
|
|
948
|
8 |
|
if ($dimension->getExtractionFunction()) { |
949
|
1 |
|
return false; |
950
|
|
|
} |
951
|
|
|
|
952
|
7 |
|
if ($dimension->getDimension() != $dimension->getOutputName()) { |
953
|
3 |
|
return false; |
954
|
|
|
} |
955
|
|
|
} |
956
|
|
|
|
957
|
12 |
|
return true; |
958
|
|
|
} |
959
|
|
|
//</editor-fold> |
960
|
|
|
} |
961
|
|
|
|
962
|
|
|
|