Passed
Pull Request — master (#20)
by Teye
08:15
created

QueryBuilder::whereFlags()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 34
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 34
ccs 10
cts 10
cp 1
rs 9.9332
cc 3
nc 2
nop 3
crap 3
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
    protected $placeholders = [];
99
100
    /**
101
     * QueryBuilder constructor.
102
     *
103
     * @param \Level23\Druid\DruidClient $client
104
     * @param string                     $dataSource
105
     * @param string                     $granularity
106
     */
107 271
    public function __construct(DruidClient $client, string $dataSource, string $granularity = Granularity::ALL)
108
    {
109 271
        $this->client      = $client;
110 271
        $this->dataSource  = $dataSource;
111 271
        $this->granularity = Granularity::validate($granularity);
112 270
    }
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
     * presents itself as a single column.
121
     *
122
     * @param string $expression
123
     * @param string $as
124
     * @param string $outputType
125
     *
126
     * @return $this
127
     * @see https://druid.apache.org/docs/latest/misc/math-expr.html
128
     */
129 1
    public function selectVirtual(string $expression, string $as, $outputType = DataType::STRING)
130
    {
131 1
        $this->virtualColumns[] = new VirtualColumn($expression, $as, $outputType);
132
133 1
        $this->select($as);
134
135 1
        return $this;
136
    }
137
138
    /**
139
     * Execute a druid query. We will try to detect the best possible query type possible.
140
     *
141
     * @param array|QueryContext $context
142
     *
143
     * @return QueryResponse
144
     * @throws \Level23\Druid\Exceptions\QueryResponseException
145
     */
146 1
    public function execute($context = []): QueryResponse
147
    {
148 1
        $query = $this->buildQuery($context);
149
150 1
        $rawResponse = $this->client->executeQuery($query);
151
152 1
        return $query->parseResponse($rawResponse);
153
    }
154
155
    /**
156
     * Update/set the dataSource
157
     *
158
     * @param string $dataSource
159
     *
160
     * @return $this
161
     */
162 28
    public function dataSource(string $dataSource): QueryBuilder
163
    {
164 28
        $this->dataSource = $dataSource;
165
166 28
        return $this;
167
    }
168
169
    /**
170
     * Update/set the granularity
171
     *
172
     * @param string $granularity
173
     *
174
     * @return $this
175
     */
176 17
    public function granularity(string $granularity): QueryBuilder
177
    {
178 17
        $this->granularity = Granularity::validate($granularity);
179
180 17
        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
     * @param string $dimension The dimension which contains int values where you want to do a bitwise AND check
197
     *                          against.
198
     * @param int    $flags     The bit's which you want to check if they are enabled in the given dimension.
199
     *
200
     * @return $this
201
     */
202 1
    public function orWhereFlags(string $dimension, int $flags)
203
    {
204 1
        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
     * 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
     *
216
     * JavaScript-based functionality is disabled by default. Please refer to the Druid JavaScript programming guide
217
     * 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
     */
228 3
    public function whereFlags(string $dimension, int $flags, string $boolean = 'and')
229
    {
230
        /**
231
         * If our version supports this, let's use the new and improved bitwiseAnd function!
232
         */
233 3
        $version = (string)$this->client->config('version');
234 3
        if (!empty($version) && version_compare($version, '0.20.2', '>=')) {
235
236 1
            $placeholder          = 'v' . count($this->placeholders);
237 1
            $this->placeholders[] = $placeholder;
238
239 1
            $this->virtualColumn('bitwiseAnd("' . $dimension . '", ' . $flags . ')', $placeholder, DataType::LONG);
240
241 1
            return $this->where($placeholder, '=', $flags, null, $boolean);
242
        }
243
244
        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
            // $flags, or 0 when it's bit is not set.
247 2
            $extraction->javascript('
248
                function(dimensionValue) { 
249 2
                    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
                    var l = low1 & low2; 
258
                    return (h*hi + l); 
259
                }
260
            ');
261 2
        }, $boolean);
262
    }
263
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
     *
274
     * Example:
275
     * 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 3
    public function subtotals(array $subtotals)
287
    {
288 3
        $this->subtotals = $subtotals;
289
290 3
        return $this;
291
    }
292
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 1
    public function metrics(array $metrics)
304
    {
305 1
        $this->metrics = $metrics;
306
307 1
        return $this;
308
    }
309
310
    /**
311
     * Set a paging identifier. This is only applied for a SELECT query!
312
     *
313
     * @param array $pagingIdentifier
314
     *
315
     * @return \Level23\Druid\Queries\QueryBuilder
316
     */
317 5
    public function pagingIdentifier(array $pagingIdentifier): QueryBuilder
318
    {
319 5
        $this->pagingIdentifier = $pagingIdentifier;
320
321 5
        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
     */
330 1
    public function segmentMetadata(): SegmentMetadataQueryResponse
331
    {
332 1
        $query = new SegmentMetadataQuery($this->dataSource, new IntervalCollection(...$this->intervals));
333
334 1
        $rawResponse = $this->client->executeQuery($query);
335
336 1
        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
     */
347 1
    public function toJson($context = []): string
348
    {
349 1
        $query = $this->buildQuery($context);
350
351 1
        $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 1
        return (string)$json;
359
    }
360
361
    /**
362
     * Return the query as an array
363
     *
364
     * @param array|QueryContext $context
365
     *
366
     * @return array
367
     */
368 2
    public function toArray($context = []): array
369
    {
370 2
        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
     */
381 1
    public function timeseries($context = []): TimeSeriesQueryResponse
382
    {
383 1
        $query = $this->buildTimeSeriesQuery($context);
384
385 1
        $rawResponse = $this->client->executeQuery($query);
386
387 1
        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
     * @param string                 $resultFormat Result Format. Use one of the ScanQueryResultFormat::* constants.
399
     *
400
     * @return ScanQueryResponse
401
     * @throws \Level23\Druid\Exceptions\QueryResponseException
402
     */
403 5
    public function scan(
404
        $context = [],
405
        int $rowBatchSize = null,
406
        bool $legacy = false,
407
        string $resultFormat = ScanQueryResultFormat::NORMAL_LIST
408
    ): ScanQueryResponse {
409 5
        $query = $this->buildScanQuery($context, $rowBatchSize, $legacy, $resultFormat);
410
411 2
        $rawResponse = $this->client->executeQuery($query);
412
413 2
        return $query->parseResponse($rawResponse);
414
    }
415
416
    /**
417
     * Execute a select query.
418
     *
419
     * @param array|QueryContext $context
420
     *
421
     * @return SelectQueryResponse
422
     * @throws \Level23\Druid\Exceptions\QueryResponseException
423
     */
424 3
    public function selectQuery($context = []): SelectQueryResponse
425
    {
426 3
        $query = $this->buildSelectQuery($context);
427
428 1
        $rawResponse = $this->client->executeQuery($query);
429
430 1
        return $query->parseResponse($rawResponse);
431
    }
432
433
    /**
434
     * Execute a topN query.
435
     *
436
     * @param array|TopNQueryContext $context
437
     *
438
     * @return TopNQueryResponse
439
     * @throws \Level23\Druid\Exceptions\QueryResponseException
440
     */
441 4
    public function topN($context = []): TopNQueryResponse
442
    {
443 4
        $query = $this->buildTopNQuery($context);
444
445 1
        $rawResponse = $this->client->executeQuery($query);
446
447 1
        return $query->parseResponse($rawResponse);
448
    }
449
450
    /**
451
     * Return the group by query
452
     *
453
     * @param array|GroupByV2QueryContext|GroupByV1QueryContext $context
454
     *
455
     * @return GroupByQueryResponse
456
     * @throws \Level23\Druid\Exceptions\QueryResponseException
457
     */
458 1
    public function groupBy($context = []): GroupByQueryResponse
459
    {
460 1
        $query = $this->buildGroupByQuery($context, 'v2');
461
462 1
        $rawResponse = $this->client->executeQuery($query);
463
464 1
        return $query->parseResponse($rawResponse);
465
    }
466
467
    /**
468
     * Return the group by query
469
     *
470
     * @param array|GroupByV2QueryContext|GroupByV1QueryContext $context
471
     *
472
     * @return GroupByQueryResponse
473
     * @throws \Level23\Druid\Exceptions\QueryResponseException
474
     */
475 1
    public function groupByV1($context = []): GroupByQueryResponse
476
    {
477 1
        $query = $this->buildGroupByQuery($context, 'v1');
478
479 1
        $rawResponse = $this->client->executeQuery($query);
480
481 1
        return $query->parseResponse($rawResponse);
482
    }
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
     */
493 3
    public function search($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQueryResponse
494
    {
495 3
        $query = $this->buildSearchQuery($context, $sortingOrder);
496
497 1
        $rawResponse = $this->client->executeQuery($query);
498
499 1
        return $query->parseResponse($rawResponse);
500
    }
501
502
    //<editor-fold desc="Protected methods">
503
504
    /**
505
     * In our previous version we required an `order()` with "__time" for TimeSeries, Scan and Select Queries.
506
     * This method makes sure that we are backwards compatible.
507
     *
508
     * @param string|null $dimension If given, we will also check if this dimension was ordered by.
509
     *
510
     * @return bool|null
511
     */
512 9
    protected function legacyIsOrderByDirectionDescending(string $dimension = null): ?bool
513
    {
514 9
        if ($this->limit) {
515 7
            $orderBy = $this->limit->getOrderByCollection();
516
517 7
            if ($orderBy->count() > 0) {
518 5
                $orderByItems = $orderBy->toArray();
519 5
                $first        = reset($orderByItems);
520
521 5
                if ($first['dimension'] == '__time' || ($dimension && $dimension == $first['dimension'])) {
522 4
                    return $first['direction'] == OrderByDirection::DESC;
523
                }
524
            }
525
        }
526
527 5
        return null;
528
    }
529
530
    /**
531
     * Build a search query.
532
     *
533
     * @param array|QueryContext $context
534
     * @param string             $sortingOrder
535
     *
536
     * @return \Level23\Druid\Queries\SearchQuery
537
     */
538 5
    protected function buildSearchQuery($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQuery
539
    {
540 5
        if (count($this->intervals) == 0) {
541 1
            throw new InvalidArgumentException('You have to specify at least one interval');
542
        }
543
544 4
        if (!$this->searchFilter) {
545 1
            throw new InvalidArgumentException('You have to specify a search filter!');
546
        }
547
548 3
        $query = new SearchQuery(
549 3
            $this->dataSource,
550 3
            $this->granularity,
551 3
            new IntervalCollection(...$this->intervals),
552 3
            $this->searchFilter
553
        );
554
555 3
        if (count($this->searchDimensions) > 0) {
556 1
            $query->setDimensions($this->searchDimensions);
557
        }
558
559 3
        if (is_array($context) && count($context) > 0) {
560 1
            $query->setContext(new QueryContext($context));
561 2
        } elseif ($context instanceof QueryContext) {
562 1
            $query->setContext($context);
563
        }
564
565 3
        if ($this->filter) {
566 2
            $query->setFilter($this->filter);
567
        }
568
569 3
        if ($sortingOrder) {
570 3
            $query->setSort($sortingOrder);
571
        }
572
573 3
        if ($this->limit && $this->limit->getLimit() !== null) {
574 2
            $query->setLimit($this->limit->getLimit());
575
        }
576
577 3
        return $query;
578
    }
579
580
    /**
581
     * Build a select query.
582
     *
583
     * @param array|QueryContext $context
584
     *
585
     * @return \Level23\Druid\Queries\SelectQuery
586
     */
587 6
    protected function buildSelectQuery($context = []): SelectQuery
588
    {
589 6
        if (count($this->intervals) == 0) {
590 1
            throw new InvalidArgumentException('You have to specify at least one interval');
591
        }
592
593 5
        if (!$this->limit || $this->limit->getLimit() === null) {
594 1
            throw new InvalidArgumentException('You have to supply a limit');
595
        }
596
597 4
        $limit = $this->limit->getLimit();
598
599 4
        $descending = false;
600 4
        if ($this->direction) {
601 1
            $descending = ($this->direction === OrderByDirection::DESC);
602 3
        } elseif ($this->legacyIsOrderByDirectionDescending() === true) {
603 1
            $descending = true;
604
        }
605
606 4
        $query = new SelectQuery(
607 4
            $this->dataSource,
608 4
            new IntervalCollection(...$this->intervals),
609 4
            $limit,
610 4
            count($this->dimensions) > 0 ? new DimensionCollection(...$this->dimensions) : null,
611 4
            $this->metrics,
612 4
            $descending
613
        );
614
615 4
        if ($this->pagingIdentifier) {
616 2
            $query->setPagingIdentifier($this->pagingIdentifier);
617
        }
618
619 4
        if (is_array($context) && count($context) > 0) {
620 1
            $query->setContext(new QueryContext($context));
621 3
        } elseif ($context instanceof QueryContext) {
622 2
            $query->setContext($context);
623
        }
624
625 4
        return $query;
626
    }
627
628
    /**
629
     * Build a scan query.
630
     *
631
     * @param array|QueryContext $context
632
     * @param int|null           $rowBatchSize
633
     * @param bool               $legacy
634
     * @param string             $resultFormat
635
     *
636
     * @return \Level23\Druid\Queries\ScanQuery
637
     */
638 10
    protected function buildScanQuery(
639
        $context = [],
640
        int $rowBatchSize = null,
641
        bool $legacy = false,
642
        string $resultFormat = ScanQueryResultFormat::NORMAL_LIST
643
    ) {
644 10
        if (count($this->intervals) == 0) {
645 1
            throw new InvalidArgumentException('You have to specify at least one interval');
646
        }
647
648 9
        if (!$this->isDimensionsListScanCompliant()) {
649 1
            throw new InvalidArgumentException(
650
                'Only simple dimension or metric selects are available in a scan query. ' .
651 1
                'Aliases, extractions or lookups are not available.'
652
            );
653
        }
654
655 8
        $query = new ScanQuery(
656 8
            $this->dataSource,
657 8
            new IntervalCollection(...$this->intervals)
658
        );
659
660 8
        $columns = [];
661 8
        foreach ($this->dimensions as $dimension) {
662 3
            $columns[] = $dimension->getDimension();
663
        }
664
665 8
        if ($this->direction) {
666 2
            $query->setOrder($this->direction);
667
        } else {
668 6
            $isDescending = $this->legacyIsOrderByDirectionDescending();
669 6
            if ($isDescending !== null) {
670 2
                $query->setOrder($isDescending ? OrderByDirection::DESC : OrderByDirection::ASC);
671
            }
672
        }
673
674 8
        if (count($columns) > 0) {
675 3
            $query->setColumns($columns);
676
        }
677
678 8
        if ($this->filter) {
679 4
            $query->setFilter($this->filter);
680
        }
681
682 8
        if ($this->limit && $this->limit->getLimit() !== null) {
683 5
            $query->setLimit($this->limit->getLimit());
684
        }
685
686 8
        if ($this->limit && $this->limit->getOffset() !== null) {
687 3
            $query->setOffset($this->limit->getOffset());
688
        }
689
690 8
        if (is_array($context) && count($context) > 0) {
691 1
            $query->setContext(new ScanQueryContext($context));
692 7
        } elseif ($context instanceof QueryContext) {
693 2
            $query->setContext($context);
694
        }
695
696 8
        if ($resultFormat) {
697 8
            $query->setResultFormat($resultFormat);
698
        }
699
700 7
        if ($rowBatchSize !== null && $rowBatchSize > 0) {
701 3
            $query->setBatchSize($rowBatchSize);
702
        }
703
704 7
        $query->setLegacy($legacy);
705
706 7
        return $query;
707
    }
708
709
    /**
710
     * Build a TimeSeries query.
711
     *
712
     * @param array|QueryContext $context
713
     *
714
     * @return TimeSeriesQuery
715
     */
716 8
    protected function buildTimeSeriesQuery($context = []): TimeSeriesQuery
717
    {
718 8
        if (count($this->intervals) == 0) {
719 1
            throw new InvalidArgumentException('You have to specify at least one interval');
720
        }
721
722 7
        $query = new TimeSeriesQuery(
723 7
            $this->dataSource,
724 7
            new IntervalCollection(...$this->intervals),
725 7
            $this->granularity
726
        );
727
728
        // check if we want to use a different output name for the __time column
729 7
        $dimension = null;
730 7
        if (count($this->dimensions) == 1) {
731 7
            $dimension = $this->dimensions[0];
732
            // did we only retrieve the time dimension?
733 7
            if ($dimension->getDimension() == '__time' && $dimension->getOutputName() != '__time') {
734 6
                $query->setTimeOutputName($dimension->getOutputName());
735
            }
736
        }
737
738 7
        if (is_array($context) && count($context) > 0) {
739 3
            $query->setContext(new TimeSeriesQueryContext($context));
740 4
        } elseif ($context instanceof QueryContext) {
741 4
            $query->setContext($context);
742
        }
743
744 7
        if ($this->filter) {
745 1
            $query->setFilter($this->filter);
746
        }
747
748 7
        if (count($this->aggregations) > 0) {
749 1
            $query->setAggregations(new AggregationCollection(...$this->aggregations));
750
        }
751
752 7
        if (count($this->postAggregations) > 0) {
753 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
754
        }
755
756 7
        if (count($this->virtualColumns) > 0) {
757 2
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
758
        }
759
760
        // If there is a limit set, then apply this on the time series query.
761 7
        if ($this->limit && $this->limit->getLimit() !== null) {
762 6
            $query->setLimit($this->limit->getLimit());
763
        }
764
765 7
        $descending = false;
766 7
        if ($this->direction) {
767 5
            $descending = ($this->direction === OrderByDirection::DESC);
768 2
        } elseif ($this->legacyIsOrderByDirectionDescending($dimension ? $dimension->getOutputName() : null) === true) {
769 1
            $descending = true;
770
        }
771
772 7
        if ($descending) {
773 2
            $query->setDescending($descending);
774
        }
775
776 7
        return $query;
777
    }
778
779
    /**
780
     * Build a topN query.
781
     *
782
     * @param array|QueryContext $context
783
     *
784
     * @return TopNQuery
785
     */
786 6
    protected function buildTopNQuery($context = []): TopNQuery
787
    {
788 6
        if (count($this->intervals) == 0) {
789 1
            throw new InvalidArgumentException('You have to specify at least one interval');
790
        }
791
792 5
        if (!$this->limit instanceof LimitInterface || $this->limit->getLimit() === null) {
793 1
            throw new InvalidArgumentException(
794 1
                'You should specify a limit to make use of a top query'
795
            );
796
        }
797
798 4
        $orderByCollection = $this->limit->getOrderByCollection();
799 4
        if (count($orderByCollection) == 0) {
800 1
            throw new InvalidArgumentException(
801 1
                'You should specify a an order by direction to make use of a top query'
802
            );
803
        }
804
805
        /**
806
         * @var \Level23\Druid\OrderBy\OrderBy $orderBy
807
         */
808 3
        $orderBy = $orderByCollection[0];
809
810 3
        $metric = $orderBy->getDimension();
811
812
        /** @var \Level23\Druid\OrderBy\OrderByInterface $orderBy */
813 3
        $query = new TopNQuery(
814 3
            $this->dataSource,
815 3
            new IntervalCollection(...$this->intervals),
816 3
            $this->dimensions[0],
817 3
            $this->limit->getLimit(),
818 3
            $metric,
819 3
            $this->granularity
820
        );
821
822 3
        $query->setDescending(
823 3
            ($orderBy->getDirection() == OrderByDirection::DESC)
824
        );
825
826 3
        if (count($this->aggregations) > 0) {
827 2
            $query->setAggregations(new AggregationCollection(...$this->aggregations));
828
        }
829
830 3
        if (count($this->postAggregations) > 0) {
831 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
832
        }
833
834 3
        if (count($this->virtualColumns) > 0) {
835 2
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
836
        }
837
838 3
        if (is_array($context) && count($context) > 0) {
839 1
            $query->setContext(new TopNQueryContext($context));
840 2
        } elseif ($context instanceof QueryContext) {
841 1
            $query->setContext($context);
842
        }
843
844 3
        if ($this->filter) {
845 2
            $query->setFilter($this->filter);
846
        }
847
848 3
        return $query;
849
    }
850
851
    /**
852
     * Build the group by query
853
     *
854
     * @param array|QueryContext $context
855
     * @param string             $type
856
     *
857
     * @return GroupByQuery
858
     */
859 5
    protected function buildGroupByQuery($context = [], string $type = 'v2'): GroupByQuery
860
    {
861 5
        if (count($this->intervals) == 0) {
862 1
            throw new InvalidArgumentException('You have to specify at least one interval');
863
        }
864
865 4
        $query = new GroupByQuery(
866 4
            $this->dataSource,
867 4
            new DimensionCollection(...$this->dimensions),
868 4
            new IntervalCollection(...$this->intervals),
869 4
            new AggregationCollection(...$this->aggregations),
870 4
            $this->granularity
871
        );
872
873 4
        if (is_array($context)) {
874 2
            switch ($type) {
875 2
                case 'v1':
876 1
                    $context = new GroupByV1QueryContext($context);
877 1
                    break;
878
879
                default:
880 1
                case 'v2':
881 1
                    $context = new GroupByV2QueryContext($context);
882 1
                    break;
883
            }
884
        }
885
886 4
        $query->setContext($context);
887
888 4
        if (count($this->postAggregations) > 0) {
889 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
890
        }
891
892 4
        if ($this->filter) {
893 2
            $query->setFilter($this->filter);
894
        }
895
896 4
        if ($this->limit) {
897 2
            $query->setLimit($this->limit);
898
        }
899
900 4
        if (count($this->virtualColumns) > 0) {
901 1
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
902
        }
903
904 4
        if (count($this->subtotals) > 0) {
905 2
            $query->setSubtotals($this->subtotals);
906
        }
907
908 4
        if ($this->having) {
909 2
            $query->setHaving($this->having);
910
        }
911
912 4
        return $query;
913
    }
914
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 7
    protected function buildQuery($context = []): QueryInterface
923
    {
924
        // Check if this is a scan query. This is the preferred way to query when there are
925
        // no aggregations done.
926 7
        if ($this->isScanQuery()) {
927 2
            return $this->buildScanQuery($context);
928
        }
929
930
        // 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 5
        if ($this->isTimeSeriesQuery()) {
933 1
            return $this->buildTimeSeriesQuery($context);
934
        }
935
936
        // Check if we can use a topN query.
937 4
        if ($this->isTopNQuery()) {
938 1
            return $this->buildTopNQuery($context);
939
        }
940
941
        // Check if we can use a select query.
942 3
        if ($this->isSelectQuery()) {
943 1
            return $this->buildSelectQuery($context);
944
        }
945
946
        // Check if we can use a search query.
947 2
        if ($this->isSearchQuery()) {
948 1
            return $this->buildSearchQuery($context);
949
        }
950
951 1
        return $this->buildGroupByQuery($context, 'v2');
952
    }
953
954
    /**
955
     * Determine if the current query is a TimeSeries query
956
     *
957
     * @return bool
958
     */
959 4
    protected function isTimeSeriesQuery(): bool
960
    {
961 4
        if (count($this->dimensions) != 1) {
962 1
            return false;
963
        }
964
965 3
        return $this->dimensions[0]->getDimension() == '__time'
966 3
            && $this->dimensions[0] instanceof Dimension
967 3
            && $this->dimensions[0]->getExtractionFunction() === null;
968
    }
969
970
    /**
971
     * Determine if the current query is topN query
972
     *
973
     * @return bool
974
     */
975 4
    protected function isTopNQuery(): bool
976
    {
977 4
        if (count($this->dimensions) != 1) {
978 1
            return false;
979
        }
980
981 3
        return $this->limit
982 3
            && $this->limit->getLimit() !== null
983 3
            && $this->limit->getOffset() === null
984 3
            && count($this->limit->getOrderByCollection()) == 1;
985
    }
986
987
    /**
988
     * Check if we should use a select query.
989
     *
990
     * @return bool
991
     */
992 4
    protected function isSelectQuery(): bool
993
    {
994 4
        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 2
    protected function isSearchQuery(): bool
1003
    {
1004 2
        return !empty($this->searchFilter);
1005
    }
1006
1007
    /**
1008
     * Check if we should use a scan query.
1009
     *
1010
     * @return bool
1011
     */
1012 5
    protected function isScanQuery(): bool
1013
    {
1014 5
        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 19
    protected function isDimensionsListScanCompliant(): bool
1023
    {
1024 19
        foreach ($this->dimensions as $dimension) {
1025 13
            if (!$dimension instanceof Dimension) {
1026 5
                return false;
1027
            }
1028
1029 8
            if ($dimension->getExtractionFunction()) {
1030 1
                return false;
1031
            }
1032
1033 7
            if ($dimension->getDimension() != $dimension->getOutputName()) {
1034 7
                return false;
1035
            }
1036
        }
1037
1038 10
        return true;
1039
    }
1040
    //</editor-fold>
1041
}
1042
1043