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