Issues (52)

src/Queries/QueryBuilder.php (3 issues)

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;
0 ignored issues
show
The trait Level23\Druid\Concerns\HasDataSource requires the property $dataSourceName which is not provided by Level23\Druid\Queries\QueryBuilder.
Loading history...
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();
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...
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)) {
0 ignored issues
show
The condition is_array($context) is always true.
Loading history...
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