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