Cancelled
Pull Request — master (#22)
by Teye
06:15 queued 01:50
created

QueryBuilder   F

Complexity

Total Complexity 136

Size/Duplication

Total Lines 993
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 136
eloc 291
dl 0
loc 993
ccs 294
cts 294
cp 1
rs 2
c 4
b 1
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A subtotals() 0 5 1
A search() 0 7 1
A selectQuery() 0 7 1
A whereFlags() 0 34 3
A granularity() 0 5 1
A isSearchQuery() 0 3 1
A orWhereFlags() 0 3 1
A scan() 0 11 1
A isScanQuery() 0 3 2
A toJson() 0 12 2
A isDimensionsListScanCompliant() 0 17 5
A __construct() 0 5 1
A toArray() 0 3 1
A execute() 0 7 1
B buildSearchQuery() 0 40 11
A metrics() 0 5 1
A isTopNQuery() 0 10 5
C buildTopNQuery() 0 63 12
F buildScanQuery() 0 69 19
A segmentMetadata() 0 7 1
A timeseries() 0 7 1
A topN() 0 7 1
A dataSource() 0 5 1
A pagingIdentifier() 0 5 1
A isSelectQuery() 0 3 2
A legacyIsOrderByDirectionDescending() 0 16 6
A buildQuery() 0 30 6
A isTimeSeriesQuery() 0 9 4
A groupByV1() 0 7 1
A selectVirtual() 0 7 1
A groupBy() 0 7 1
F buildTimeSeriesQuery() 0 61 18
C buildGroupByQuery() 0 54 11
B buildSelectQuery() 0 39 11

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

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