Passed
Push — master ( 4d62c3...e2457a )
by Teye
05:24
created

QueryBuilder::toJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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;
0 ignored issues
show
Bug introduced by
The trait Level23\Druid\Concerns\HasDataSource requires the property $dataSourceName which is not provided by Level23\Druid\Queries\QueryBuilder.
Loading history...
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();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getQuery($context)->toArray() returns an array which contains values of type boolean which are incompatible with the documented value type array<mixed,mixed>|integer|string.
Loading history...
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)) {
0 ignored issues
show
introduced by
The condition is_array($context) is always true.
Loading history...
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