Passed
Push — master ( 407608...46ce30 )
by Teye
05:59
created

QueryBuilder::search()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 2
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
use function json_last_error;
48
use function json_last_error_msg;
49
50
class QueryBuilder
51
{
52
    use HasFilter, HasHaving, HasDimensions, HasAggregations, HasIntervals, HasLimit, HasVirtualColumns, HasPostAggregations, HasSearchFilters, HasDataSource;
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...
53
54
    protected DruidClient $client;
55
56
    protected ?QueryBuilder $query = null;
57
58
    protected DataSourceInterface $dataSource;
59
60
    protected string $granularity;
61
62
    /**
63
     * @var array|\Level23\Druid\PostAggregations\PostAggregatorInterface[]
64
     */
65
    protected array $postAggregations = [];
66
67
    /**
68
     * Set a paging identifier for a Select query.
69
     *
70
     * @var array<string,int>|null
71
     */
72
    protected ?array $pagingIdentifier = null;
73
74
    /**
75
     * The subtotal spec (only applies for groupBy queries)
76
     *
77
     * @var array<array<string>>
78
     */
79
    protected array $subtotals = [];
80
81
    /**
82
     * The metrics to select when using a Select Query.
83
     * When empty, all metrics are returned.
84
     *
85
     * @var array<string>
86
     */
87
    protected array $metrics = [];
88
89
    /**
90
     * This contains a list of "temporary" field names which we will use to store our result of
91
     * a virtual column when the whereFlag() method is used.
92
     *
93
     * @var array<string>
94
     */
95
    public array $placeholders = [];
96
97
    /**
98
     * QueryBuilder constructor.
99
     *
100
     * @param \Level23\Druid\DruidClient $client
101
     * @param string                     $dataSource
102
     * @param string                     $granularity
103
     */
104 343
    public function __construct(DruidClient $client, string $dataSource = '', string $granularity = Granularity::ALL)
105
    {
106 343
        $this->client      = $client;
107 343
        $this->query       = $this;
108 343
        $this->dataSource  = new TableDataSource($dataSource);
109 343
        $this->granularity = Granularity::validate($granularity);
110
    }
111
112
    /**
113
     * Create a virtual column and select the result.
114
     *
115
     * Virtual columns are queryable column "views" created from a set of columns during a query.
116
     *
117
     * A virtual column can potentially draw from multiple underlying columns, although a virtual column always
118
     * presents itself as a single column.
119
     *
120
     * @param string $expression
121
     * @param string $as
122
     * @param string $outputType
123
     *
124
     * @return $this
125
     * @see https://druid.apache.org/docs/latest/misc/math-expr.html
126
     */
127 1
    public function selectVirtual(string $expression, string $as, string $outputType = DataType::STRING): self
128
    {
129 1
        $this->virtualColumn($expression, $as, $outputType);
130 1
        $this->select($as, $as, null, $outputType);
0 ignored issues
show
Bug introduced by
$as of type string is incompatible with the type ArrayObject|array<integer,string> expected by parameter $dimension of Level23\Druid\Queries\QueryBuilder::select(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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