Completed
Pull Request — master (#20)
by Teye
08:02 queued 01:46
created

QueryBuilder   F

Complexity

Total Complexity 135

Size/Duplication

Total Lines 988
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 135
eloc 288
c 4
b 1
f 0
dl 0
loc 988
ccs 309
cts 309
cp 1
rs 2

34 Methods

Rating   Name   Duplication   Size   Complexity  
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 7 1
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 search() 0 7 1
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
A subtotals() 0 5 1
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
    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 = $this->client->config('version');
234 3
        if (!empty($version) && version_compare($version, '0.20.2', '>=')) {
0 ignored issues
show
Bug introduced by
It seems like $version can also be of type mixed; however, parameter $version1 of version_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

234
        if (!empty($version) && version_compare(/** @scrutinizer ignore-type */ $version, '0.20.2', '>=')) {
Loading history...
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 = \GuzzleHttp\json_encode($query->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
352
353 1
        return $json;
354
    }
355
356
    /**
357
     * Return the query as an array
358
     *
359
     * @param array|QueryContext $context
360
     *
361
     * @return array
362
     */
363 2
    public function toArray($context = []): array
364
    {
365 2
        return $this->buildQuery($context)->toArray();
366
    }
367
368
    /**
369
     * Execute a TimeSeries query.
370
     *
371
     * @param array|TimeSeriesQueryContext $context
372
     *
373
     * @return TimeSeriesQueryResponse
374
     * @throws \Level23\Druid\Exceptions\QueryResponseException
375
     */
376 1
    public function timeseries($context = []): TimeSeriesQueryResponse
377
    {
378 1
        $query = $this->buildTimeSeriesQuery($context);
379
380 1
        $rawResponse = $this->client->executeQuery($query);
381
382 1
        return $query->parseResponse($rawResponse);
383
    }
384
385
    /**
386
     * Execute a Scan Query.
387
     *
388
     * @param array|ScanQueryContext $context      Query context parameters
389
     * @param int|null               $rowBatchSize How many rows buffered before return to client. Default is 20480
390
     * @param bool                   $legacy       Return results consistent with the legacy "scan-query" contrib
391
     *                                             extension. Defaults to the value set by druid.query.scan.legacy,
392
     *                                             which in turn defaults to false. See Legacy mode for details.
393
     * @param string                 $resultFormat Result Format. Use one of the ScanQueryResultFormat::* constants.
394
     *
395
     * @return ScanQueryResponse
396
     * @throws \Level23\Druid\Exceptions\QueryResponseException
397
     */
398 5
    public function scan(
399
        $context = [],
400
        int $rowBatchSize = null,
401
        bool $legacy = false,
402
        string $resultFormat = ScanQueryResultFormat::NORMAL_LIST
403
    ): ScanQueryResponse {
404 5
        $query = $this->buildScanQuery($context, $rowBatchSize, $legacy, $resultFormat);
405
406 2
        $rawResponse = $this->client->executeQuery($query);
407
408 2
        return $query->parseResponse($rawResponse);
409
    }
410
411
    /**
412
     * Execute a select query.
413
     *
414
     * @param array|QueryContext $context
415
     *
416
     * @return SelectQueryResponse
417
     * @throws \Level23\Druid\Exceptions\QueryResponseException
418
     */
419 3
    public function selectQuery($context = []): SelectQueryResponse
420
    {
421 3
        $query = $this->buildSelectQuery($context);
422
423 1
        $rawResponse = $this->client->executeQuery($query);
424
425 1
        return $query->parseResponse($rawResponse);
426
    }
427
428
    /**
429
     * Execute a topN query.
430
     *
431
     * @param array|TopNQueryContext $context
432
     *
433
     * @return TopNQueryResponse
434
     * @throws \Level23\Druid\Exceptions\QueryResponseException
435
     */
436 4
    public function topN($context = []): TopNQueryResponse
437
    {
438 4
        $query = $this->buildTopNQuery($context);
439
440 1
        $rawResponse = $this->client->executeQuery($query);
441
442 1
        return $query->parseResponse($rawResponse);
443
    }
444
445
    /**
446
     * Return the group by query
447
     *
448
     * @param array|GroupByV2QueryContext|GroupByV1QueryContext $context
449
     *
450
     * @return GroupByQueryResponse
451
     * @throws \Level23\Druid\Exceptions\QueryResponseException
452
     */
453 1
    public function groupBy($context = []): GroupByQueryResponse
454
    {
455 1
        $query = $this->buildGroupByQuery($context, 'v2');
456
457 1
        $rawResponse = $this->client->executeQuery($query);
458
459 1
        return $query->parseResponse($rawResponse);
460
    }
461
462
    /**
463
     * Return the group by query
464
     *
465
     * @param array|GroupByV2QueryContext|GroupByV1QueryContext $context
466
     *
467
     * @return GroupByQueryResponse
468
     * @throws \Level23\Druid\Exceptions\QueryResponseException
469
     */
470 1
    public function groupByV1($context = []): GroupByQueryResponse
471
    {
472 1
        $query = $this->buildGroupByQuery($context, 'v1');
473
474 1
        $rawResponse = $this->client->executeQuery($query);
475
476 1
        return $query->parseResponse($rawResponse);
477
    }
478
479
    /**
480
     * Execute a search query and return the response
481
     *
482
     * @param array|QueryContext $context
483
     * @param string             $sortingOrder
484
     *
485
     * @return \Level23\Druid\Responses\SearchQueryResponse
486
     * @throws \Level23\Druid\Exceptions\QueryResponseException
487
     */
488 3
    public function search($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQueryResponse
489
    {
490 3
        $query = $this->buildSearchQuery($context, $sortingOrder);
491
492 1
        $rawResponse = $this->client->executeQuery($query);
493
494 1
        return $query->parseResponse($rawResponse);
495
    }
496
497
    //<editor-fold desc="Protected methods">
498
499
    /**
500
     * In our previous version we required an `order()` with "__time" for TimeSeries, Scan and Select Queries.
501
     * This method makes sure that we are backwards compatible.
502
     *
503
     * @param string|null $dimension If given, we will also check if this dimension was ordered by.
504
     *
505
     * @return bool|null
506
     */
507 9
    protected function legacyIsOrderByDirectionDescending(string $dimension = null): ?bool
508
    {
509 9
        if ($this->limit) {
510 7
            $orderBy = $this->limit->getOrderByCollection();
511
512 7
            if ($orderBy->count() > 0) {
513 5
                $orderByItems = $orderBy->toArray();
514 5
                $first        = reset($orderByItems);
515
516 5
                if ($first['dimension'] == '__time' || ($dimension && $dimension == $first['dimension'])) {
517 4
                    return $first['direction'] == OrderByDirection::DESC;
518
                }
519
            }
520
        }
521
522 5
        return null;
523
    }
524
525
    /**
526
     * Build a search query.
527
     *
528
     * @param array|QueryContext $context
529
     * @param string             $sortingOrder
530
     *
531
     * @return \Level23\Druid\Queries\SearchQuery
532
     */
533 5
    protected function buildSearchQuery($context = [], string $sortingOrder = SortingOrder::LEXICOGRAPHIC): SearchQuery
534
    {
535 5
        if (count($this->intervals) == 0) {
536 1
            throw new InvalidArgumentException('You have to specify at least one interval');
537
        }
538
539 4
        if (!$this->searchFilter) {
540 1
            throw new InvalidArgumentException('You have to specify a search filter!');
541
        }
542
543 3
        $query = new SearchQuery(
544 3
            $this->dataSource,
545 3
            $this->granularity,
546 3
            new IntervalCollection(...$this->intervals),
547 3
            $this->searchFilter
548
        );
549
550 3
        if (count($this->searchDimensions) > 0) {
551 1
            $query->setDimensions($this->searchDimensions);
552
        }
553
554 3
        if (is_array($context) && count($context) > 0) {
555 1
            $query->setContext(new QueryContext($context));
556 2
        } elseif ($context instanceof QueryContext) {
557 1
            $query->setContext($context);
558
        }
559
560 3
        if ($this->filter) {
561 2
            $query->setFilter($this->filter);
562
        }
563
564 3
        if ($sortingOrder) {
565 3
            $query->setSort($sortingOrder);
566
        }
567
568 3
        if ($this->limit && $this->limit->getLimit() !== null) {
569 2
            $query->setLimit($this->limit->getLimit());
570
        }
571
572 3
        return $query;
573
    }
574
575
    /**
576
     * Build a select query.
577
     *
578
     * @param array|QueryContext $context
579
     *
580
     * @return \Level23\Druid\Queries\SelectQuery
581
     */
582 6
    protected function buildSelectQuery($context = []): SelectQuery
583
    {
584 6
        if (count($this->intervals) == 0) {
585 1
            throw new InvalidArgumentException('You have to specify at least one interval');
586
        }
587
588 5
        if (!$this->limit || $this->limit->getLimit() === null) {
589 1
            throw new InvalidArgumentException('You have to supply a limit');
590
        }
591
592 4
        $limit = $this->limit->getLimit();
593
594 4
        $descending = false;
595 4
        if ($this->direction) {
596 1
            $descending = ($this->direction === OrderByDirection::DESC);
597 3
        } elseif ($this->legacyIsOrderByDirectionDescending() === true) {
598 1
            $descending = true;
599
        }
600
601 4
        $query = new SelectQuery(
602 4
            $this->dataSource,
603 4
            new IntervalCollection(...$this->intervals),
604 4
            $limit,
605 4
            count($this->dimensions) > 0 ? new DimensionCollection(...$this->dimensions) : null,
606 4
            $this->metrics,
607 4
            $descending
608
        );
609
610 4
        if ($this->pagingIdentifier) {
611 2
            $query->setPagingIdentifier($this->pagingIdentifier);
612
        }
613
614 4
        if (is_array($context) && count($context) > 0) {
615 1
            $query->setContext(new QueryContext($context));
616 3
        } elseif ($context instanceof QueryContext) {
617 2
            $query->setContext($context);
618
        }
619
620 4
        return $query;
621
    }
622
623
    /**
624
     * Build a scan query.
625
     *
626
     * @param array|QueryContext $context
627
     * @param int|null           $rowBatchSize
628
     * @param bool               $legacy
629
     * @param string             $resultFormat
630
     *
631
     * @return \Level23\Druid\Queries\ScanQuery
632
     */
633 10
    protected function buildScanQuery(
634
        $context = [],
635
        int $rowBatchSize = null,
636
        bool $legacy = false,
637
        string $resultFormat = ScanQueryResultFormat::NORMAL_LIST
638
    ) {
639 10
        if (count($this->intervals) == 0) {
640 1
            throw new InvalidArgumentException('You have to specify at least one interval');
641
        }
642
643 9
        if (!$this->isDimensionsListScanCompliant()) {
644 1
            throw new InvalidArgumentException(
645
                'Only simple dimension or metric selects are available in a scan query. ' .
646 1
                'Aliases, extractions or lookups are not available.'
647
            );
648
        }
649
650 8
        $query = new ScanQuery(
651 8
            $this->dataSource,
652 8
            new IntervalCollection(...$this->intervals)
653
        );
654
655 8
        $columns = [];
656 8
        foreach ($this->dimensions as $dimension) {
657 3
            $columns[] = $dimension->getDimension();
658
        }
659
660 8
        if ($this->direction) {
661 2
            $query->setOrder($this->direction);
662
        } else {
663 6
            $isDescending = $this->legacyIsOrderByDirectionDescending();
664 6
            if ($isDescending !== null) {
665 2
                $query->setOrder($isDescending ? OrderByDirection::DESC : OrderByDirection::ASC);
666
            }
667
        }
668
669 8
        if (count($columns) > 0) {
670 3
            $query->setColumns($columns);
671
        }
672
673 8
        if ($this->filter) {
674 4
            $query->setFilter($this->filter);
675
        }
676
677 8
        if ($this->limit && $this->limit->getLimit() !== null) {
678 5
            $query->setLimit($this->limit->getLimit());
679
        }
680
681 8
        if ($this->limit && $this->limit->getOffset() !== null) {
682 3
            $query->setOffset($this->limit->getOffset());
683
        }
684
685 8
        if (is_array($context) && count($context) > 0) {
686 1
            $query->setContext(new ScanQueryContext($context));
687 7
        } elseif ($context instanceof QueryContext) {
688 2
            $query->setContext($context);
689
        }
690
691 8
        if ($resultFormat) {
692 8
            $query->setResultFormat($resultFormat);
693
        }
694
695 7
        if ($rowBatchSize !== null && $rowBatchSize > 0) {
696 3
            $query->setBatchSize($rowBatchSize);
697
        }
698
699 7
        $query->setLegacy($legacy);
700
701 7
        return $query;
702
    }
703
704
    /**
705
     * Build a TimeSeries query.
706
     *
707
     * @param array|QueryContext $context
708
     *
709
     * @return TimeSeriesQuery
710
     */
711 8
    protected function buildTimeSeriesQuery($context = []): TimeSeriesQuery
712
    {
713 8
        if (count($this->intervals) == 0) {
714 1
            throw new InvalidArgumentException('You have to specify at least one interval');
715
        }
716
717 7
        $query = new TimeSeriesQuery(
718 7
            $this->dataSource,
719 7
            new IntervalCollection(...$this->intervals),
720 7
            $this->granularity
721
        );
722
723
        // check if we want to use a different output name for the __time column
724 7
        $dimension = null;
725 7
        if (count($this->dimensions) == 1) {
726 7
            $dimension = $this->dimensions[0];
727
            // did we only retrieve the time dimension?
728 7
            if ($dimension->getDimension() == '__time' && $dimension->getOutputName() != '__time') {
729 6
                $query->setTimeOutputName($dimension->getOutputName());
730
            }
731
        }
732
733 7
        if (is_array($context) && count($context) > 0) {
734 3
            $query->setContext(new TimeSeriesQueryContext($context));
735 4
        } elseif ($context instanceof QueryContext) {
736 4
            $query->setContext($context);
737
        }
738
739 7
        if ($this->filter) {
740 1
            $query->setFilter($this->filter);
741
        }
742
743 7
        if (count($this->aggregations) > 0) {
744 1
            $query->setAggregations(new AggregationCollection(...$this->aggregations));
745
        }
746
747 7
        if (count($this->postAggregations) > 0) {
748 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
749
        }
750
751 7
        if (count($this->virtualColumns) > 0) {
752 2
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
753
        }
754
755
        // If there is a limit set, then apply this on the time series query.
756 7
        if ($this->limit && $this->limit->getLimit() !== null) {
757 6
            $query->setLimit($this->limit->getLimit());
758
        }
759
760 7
        $descending = false;
761 7
        if ($this->direction) {
762 5
            $descending = ($this->direction === OrderByDirection::DESC);
763 2
        } elseif ($this->legacyIsOrderByDirectionDescending($dimension ? $dimension->getOutputName() : null) === true) {
764 1
            $descending = true;
765
        }
766
767 7
        if ($descending) {
768 2
            $query->setDescending($descending);
769
        }
770
771 7
        return $query;
772
    }
773
774
    /**
775
     * Build a topN query.
776
     *
777
     * @param array|QueryContext $context
778
     *
779
     * @return TopNQuery
780
     */
781 6
    protected function buildTopNQuery($context = []): TopNQuery
782
    {
783 6
        if (count($this->intervals) == 0) {
784 1
            throw new InvalidArgumentException('You have to specify at least one interval');
785
        }
786
787 5
        if (!$this->limit instanceof LimitInterface || $this->limit->getLimit() === null) {
788 1
            throw new InvalidArgumentException(
789 1
                'You should specify a limit to make use of a top query'
790
            );
791
        }
792
793 4
        $orderByCollection = $this->limit->getOrderByCollection();
794 4
        if (count($orderByCollection) == 0) {
795 1
            throw new InvalidArgumentException(
796 1
                'You should specify a an order by direction to make use of a top query'
797
            );
798
        }
799
800
        /**
801
         * @var \Level23\Druid\OrderBy\OrderBy $orderBy
802
         */
803 3
        $orderBy = $orderByCollection[0];
804
805 3
        $metric = $orderBy->getDimension();
806
807
        /** @var \Level23\Druid\OrderBy\OrderByInterface $orderBy */
808 3
        $query = new TopNQuery(
809 3
            $this->dataSource,
810 3
            new IntervalCollection(...$this->intervals),
811 3
            $this->dimensions[0],
812 3
            $this->limit->getLimit(),
813 3
            $metric,
814 3
            $this->granularity
815
        );
816
817 3
        $query->setDescending(
818 3
            ($orderBy->getDirection() == OrderByDirection::DESC)
819
        );
820
821 3
        if (count($this->aggregations) > 0) {
822 2
            $query->setAggregations(new AggregationCollection(...$this->aggregations));
823
        }
824
825 3
        if (count($this->postAggregations) > 0) {
826 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
827
        }
828
829 3
        if (count($this->virtualColumns) > 0) {
830 2
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
831
        }
832
833 3
        if (is_array($context) && count($context) > 0) {
834 1
            $query->setContext(new TopNQueryContext($context));
835 2
        } elseif ($context instanceof QueryContext) {
836 1
            $query->setContext($context);
837
        }
838
839 3
        if ($this->filter) {
840 2
            $query->setFilter($this->filter);
841
        }
842
843 3
        return $query;
844
    }
845
846
    /**
847
     * Build the group by query
848
     *
849
     * @param array|QueryContext $context
850
     * @param string             $type
851
     *
852
     * @return GroupByQuery
853
     */
854 5
    protected function buildGroupByQuery($context = [], string $type = 'v2'): GroupByQuery
855
    {
856 5
        if (count($this->intervals) == 0) {
857 1
            throw new InvalidArgumentException('You have to specify at least one interval');
858
        }
859
860 4
        $query = new GroupByQuery(
861 4
            $this->dataSource,
862 4
            new DimensionCollection(...$this->dimensions),
863 4
            new IntervalCollection(...$this->intervals),
864 4
            new AggregationCollection(...$this->aggregations),
865 4
            $this->granularity
866
        );
867
868 4
        if (is_array($context)) {
869 2
            switch ($type) {
870 2
                case 'v1':
871 1
                    $context = new GroupByV1QueryContext($context);
872 1
                    break;
873
874
                default:
875 1
                case 'v2':
876 1
                    $context = new GroupByV2QueryContext($context);
877 1
                    break;
878
            }
879
        }
880
881 4
        $query->setContext($context);
882
883 4
        if (count($this->postAggregations) > 0) {
884 2
            $query->setPostAggregations(new PostAggregationCollection(...$this->postAggregations));
885
        }
886
887 4
        if ($this->filter) {
888 2
            $query->setFilter($this->filter);
889
        }
890
891 4
        if ($this->limit) {
892 2
            $query->setLimit($this->limit);
893
        }
894
895 4
        if (count($this->virtualColumns) > 0) {
896 1
            $query->setVirtualColumns(new VirtualColumnCollection(...$this->virtualColumns));
897
        }
898
899 4
        if (count($this->subtotals) > 0) {
900 2
            $query->setSubtotals($this->subtotals);
901
        }
902
903 4
        if ($this->having) {
904 2
            $query->setHaving($this->having);
905
        }
906
907 4
        return $query;
908
    }
909
910
    /**
911
     * Return the query automatically detected based on the requested data.
912
     *
913
     * @param array|QueryContext $context
914
     *
915
     * @return \Level23\Druid\Queries\QueryInterface
916
     */
917 7
    protected function buildQuery($context = []): QueryInterface
918
    {
919
        // Check if this is a scan query. This is the preferred way to query when there are
920
        // no aggregations done.
921 7
        if ($this->isScanQuery()) {
922 2
            return $this->buildScanQuery($context);
923
        }
924
925
        // If we only have "grouped" by __time, then we can use a time series query.
926
        // This is preferred, because it's a lot faster then doing a group by query.
927 5
        if ($this->isTimeSeriesQuery()) {
928 1
            return $this->buildTimeSeriesQuery($context);
929
        }
930
931
        // Check if we can use a topN query.
932 4
        if ($this->isTopNQuery()) {
933 1
            return $this->buildTopNQuery($context);
934
        }
935
936
        // Check if we can use a select query.
937 3
        if ($this->isSelectQuery()) {
938 1
            return $this->buildSelectQuery($context);
939
        }
940
941
        // Check if we can use a search query.
942 2
        if ($this->isSearchQuery()) {
943 1
            return $this->buildSearchQuery($context);
944
        }
945
946 1
        return $this->buildGroupByQuery($context, 'v2');
947
    }
948
949
    /**
950
     * Determine if the current query is a TimeSeries query
951
     *
952
     * @return bool
953
     */
954 4
    protected function isTimeSeriesQuery(): bool
955
    {
956 4
        if (count($this->dimensions) != 1) {
957 1
            return false;
958
        }
959
960 3
        return $this->dimensions[0]->getDimension() == '__time'
961 3
            && $this->dimensions[0] instanceof Dimension
962 3
            && $this->dimensions[0]->getExtractionFunction() === null;
963
    }
964
965
    /**
966
     * Determine if the current query is topN query
967
     *
968
     * @return bool
969
     */
970 4
    protected function isTopNQuery(): bool
971
    {
972 4
        if (count($this->dimensions) != 1) {
973 1
            return false;
974
        }
975
976 3
        return $this->limit
977 3
            && $this->limit->getLimit() !== null
978 3
            && $this->limit->getOffset() === null
979 3
            && count($this->limit->getOrderByCollection()) == 1;
980
    }
981
982
    /**
983
     * Check if we should use a select query.
984
     *
985
     * @return bool
986
     */
987 4
    protected function isSelectQuery(): bool
988
    {
989 4
        return $this->pagingIdentifier !== null && count($this->aggregations) == 0;
990
    }
991
992
    /**
993
     * Check if we should use a search query.
994
     *
995
     * @return bool
996
     */
997 2
    protected function isSearchQuery(): bool
998
    {
999 2
        return !empty($this->searchFilter);
1000
    }
1001
1002
    /**
1003
     * Check if we should use a scan query.
1004
     *
1005
     * @return bool
1006
     */
1007 5
    protected function isScanQuery(): bool
1008
    {
1009 5
        return count($this->aggregations) == 0 && $this->isDimensionsListScanCompliant();
1010
    }
1011
1012
    /**
1013
     * Return true if the dimensions which are selected can be used as "columns" in a scan query.
1014
     *
1015
     * @return bool
1016
     */
1017 19
    protected function isDimensionsListScanCompliant(): bool
1018
    {
1019 19
        foreach ($this->dimensions as $dimension) {
1020 13
            if (!$dimension instanceof Dimension) {
1021 5
                return false;
1022
            }
1023
1024 8
            if ($dimension->getExtractionFunction()) {
1025 1
                return false;
1026
            }
1027
1028 7
            if ($dimension->getDimension() != $dimension->getOutputName()) {
1029 7
                return false;
1030
            }
1031
        }
1032
1033 10
        return true;
1034
    }
1035
    //</editor-fold>
1036
}
1037
1038