Passed
Push — master ( 4d62c3...e2457a )
by Teye
05:24
created

HasFilter::whereNotIn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 9
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 4
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Level23\Druid\Concerns;
5
6
use Closure;
7
use InvalidArgumentException;
8
use Level23\Druid\Types\DataType;
9
use Level23\Druid\Filters\InFilter;
10
use Level23\Druid\Filters\OrFilter;
11
use Level23\Druid\Filters\AndFilter;
12
use Level23\Druid\Filters\NotFilter;
13
use Level23\Druid\Interval\Interval;
14
use Level23\Druid\Filters\LikeFilter;
15
use Level23\Druid\Types\SortingOrder;
16
use Level23\Druid\Filters\BoundFilter;
17
use Level23\Druid\Filters\RegexFilter;
18
use Level23\Druid\Filters\SearchFilter;
19
use Level23\Druid\Dimensions\Dimension;
20
use Level23\Druid\Queries\QueryBuilder;
21
use Level23\Druid\Filters\FilterBuilder;
22
use Level23\Druid\Filters\BetweenFilter;
23
use Level23\Druid\Filters\IntervalFilter;
24
use Level23\Druid\Filters\SelectorFilter;
25
use Level23\Druid\Filters\FilterInterface;
26
use Level23\Druid\Filters\JavascriptFilter;
27
use Level23\Druid\Filters\ExpressionFilter;
28
use Level23\Druid\Interval\IntervalInterface;
29
use Level23\Druid\Dimensions\DimensionBuilder;
30
use Level23\Druid\Filters\SpatialRadiusFilter;
31
use Level23\Druid\Filters\SpatialPolygonFilter;
32
use Level23\Druid\Extractions\ExtractionBuilder;
33
use Level23\Druid\Dimensions\DimensionInterface;
34
use Level23\Druid\Filters\ColumnComparisonFilter;
35
use Level23\Druid\Extractions\ExtractionInterface;
36
use Level23\Druid\Filters\SpatialRectangularFilter;
37
38
trait HasFilter
39
{
40
    protected ?QueryBuilder $query = null;
41
42
    protected ?FilterInterface $filter = null;
43
44
    /**
45
     * Filter our results where the given dimension matches the value based on the operator.
46
     * The operator can be '=', '>', '>=', '<', '<=', '<>', '!=', 'like', 'not like', 'regex', 'not regex',
47
     * 'javascript', 'not javascript', 'search' and 'not search'
48
     *
49
     * @param \Closure|string|FilterInterface $filterOrDimensionOrClosure The dimension which you want to filter.
50
     * @param int|string|null                 $operator                   The operator which you want to use to filter.
51
     *                                                                    See below for a complete list of supported
52
     *                                                                    operators.
53
     * @param int|string|string[]|null|float|bool $value                      The value which you want to use in your
54
     *                                                                    filter comparison
55
     * @param \Closure|null                   $extraction                 A closure which builds one or more extraction
56
     *                                                                    function. These are applied before the filter
57
     *                                                                    will be applied. So the filter will use the
58
     *                                                                    value returned by the extraction function(s).
59
     * @param string                          $boolean                    This influences how this filter will be
60
     *                                                                    joined with previous added filters. Should
61
     *                                                                    both filters apply ("and") or one or the
62
     *                                                                    other ("or") ? Default is "and".
63
     *
64
     * @return $this
65
     */
66 59
    public function where(
67
        Closure|string|FilterInterface $filterOrDimensionOrClosure,
68
        int|string $operator = null,
69
        array|int|string|float|bool $value = null,
70
        Closure $extraction = null,
71
        string $boolean = 'and'
72
    ): self {
73 59
        $filter = null;
74 59
        if (is_string($filterOrDimensionOrClosure)) {
75 49
            if ($value === null && $operator !== null) {
76 3
                $value    = $operator;
77 3
                $operator = '=';
78
            }
79
80 49
            if ($operator === null || $value === null) {
81 2
                throw new InvalidArgumentException('You have to supply an operator and an compare value when you supply a dimension as string');
82
            }
83
84 47
            $operator = strtolower((string)$operator);
85 47
            if (is_array($value) && !in_array($operator, ['search', 'not search'])) {
86 2
                throw new InvalidArgumentException('Given $value is invalid in combination with operator ' . $operator);
87
            }
88
89
            /** @var string|int $value */
90
91 45
            if ($operator == '=') {
92 18
                $filter = new SelectorFilter(
93 18
                    $filterOrDimensionOrClosure,
94 18
                    (string)$value,
95 18
                    $this->getExtraction($extraction)
96 18
                );
97 27
            } elseif ($operator == '<>' || $operator == '!=') {
98 5
                $filter = new NotFilter(
99 5
                    new SelectorFilter($filterOrDimensionOrClosure, (string)$value, $this->getExtraction($extraction))
100 5
                );
101 22
            } elseif (in_array($operator, ['>', '>=', '<', '<='])) {
102 5
                $filter = new BoundFilter(
103 5
                    $filterOrDimensionOrClosure,
104 5
                    $operator,
105 5
                    (string)$value,
106 5
                    null,
107 5
                    $this->getExtraction($extraction)
108 5
                );
109 17
            } elseif ($operator == 'like') {
110 1
                $filter = new LikeFilter(
111 1
                    $filterOrDimensionOrClosure, (string)$value, '\\', $this->getExtraction($extraction)
112 1
                );
113 16
            } elseif ($operator == 'not like') {
114 1
                $filter = new NotFilter(
115 1
                    new LikeFilter($filterOrDimensionOrClosure, (string)$value, '\\', $this->getExtraction($extraction))
116 1
                );
117 15
            } elseif ($operator == 'javascript') {
118 1
                $filter = new JavascriptFilter($filterOrDimensionOrClosure, (string)$value,
119 1
                    $this->getExtraction($extraction));
120 14
            } elseif ($operator == 'not javascript') {
121 1
                $filter = new NotFilter(
122 1
                    new JavascriptFilter($filterOrDimensionOrClosure, (string)$value, $this->getExtraction($extraction))
123 1
                );
124 13
            } elseif ($operator == 'regex' || $operator == 'regexp') {
125 2
                $filter = new RegexFilter($filterOrDimensionOrClosure, (string)$value,
126 2
                    $this->getExtraction($extraction));
127 11
            } elseif ($operator == 'not regex' || $operator == 'not regexp') {
128 2
                $filter = new NotFilter(
129 2
                    new RegexFilter($filterOrDimensionOrClosure, (string)$value, $this->getExtraction($extraction))
130 2
                );
131 9
            } elseif ($operator == 'search') {
132 4
                if (is_int($value)) {
133 1
                    $value = (string)$value;
134
                }
135 4
                $filter = new SearchFilter(
136 4
                    $filterOrDimensionOrClosure, $value, false, $this->getExtraction($extraction)
137 4
                );
138 5
            } elseif ($operator == 'not search') {
139 4
                if (is_int($value)) {
140 1
                    $value = (string)$value;
141
                }
142 45
                $filter = new NotFilter(new SearchFilter(
143 45
                    $filterOrDimensionOrClosure, $value, false, $this->getExtraction($extraction)
144 45
                ));
145
            }
146 13
        } elseif ($filterOrDimensionOrClosure instanceof FilterInterface) {
147 11
            $filter = $filterOrDimensionOrClosure;
148 3
        } elseif ($filterOrDimensionOrClosure instanceof Closure) {
149
150
            // let's create a new builder object where the user can mess around with
151 3
            $builder = new FilterBuilder($this->query);
152
153
            // call the user function
154 3
            call_user_func($filterOrDimensionOrClosure, $builder);
155
156
            // Now retrieve the filter which was created and add it to our current filter set.
157 3
            $filter = $builder->getFilter();
158
        }
159
160 55
        if ($filter === null) {
161 1
            throw new InvalidArgumentException('The arguments which you have supplied cannot be parsed.');
162
        }
163
164 54
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
165
166 54
        return $this;
167
    }
168
169
    /**
170
     * Build a where selection which is inverted
171
     *
172
     * @param \Closure $filterBuilder A closure which will receive a FilterBuilder instance.
173
     * @param string   $boolean       This influences how this filter will be joined with previous added filters.
174
     *                                Should both filters apply ("and") or one or the other ("or") ? Default is "and".
175
     *
176
     * @return $this
177
     */
178 3
    public function whereNot(Closure $filterBuilder, string $boolean = 'and'): self
179
    {
180
        // let's create a bew builder object where the user can mess around with
181 3
        $builder = new FilterBuilder($this->query);
182
183
        // call the user function
184 3
        call_user_func($filterBuilder, $builder);
185
186
        // Now retrieve the filter which was created and add it to our current filter set.
187 3
        $filter = $builder->getFilter();
188 3
        if ($filter) {
189 2
            return $this->where(new NotFilter($filter), null, null, null, $boolean);
190
        }
191
192
        // Whe no filter was given, just return.
193 1
        return $this;
194
    }
195
196
    /**
197
     * Build a where selection which is inverted
198
     *
199
     * @param \Closure $filterBuilder A closure which will receive a FilterBuilder instance.
200
     *
201
     * @return $this
202
     */
203 1
    public function orWhereNot(Closure $filterBuilder): self
204
    {
205 1
        return $this->whereNot($filterBuilder, 'or');
206
    }
207
208
    /**
209
     * This applies a filter, only it will join previous added filters with an "or" instead of an "and".
210
     * See the documentation of the "where" method for more information
211
     *
212
     * @param string|FilterInterface   $filterOrDimension
213
     * @param string|null              $operator
214
     * @param int|string|string[]|null $value
215
     * @param \Closure|null            $extraction
216
     *
217
     * @return $this
218
     * @see \Level23\Druid\Concerns\HasFilter::where()
219
     */
220 3
    public function orWhere(
221
        string|FilterInterface $filterOrDimension,
222
        string $operator = null,
223
        array|int|string $value = null,
224
        Closure $extraction = null
225
    ): self {
226 3
        return $this->where($filterOrDimension, $operator, $value, $extraction, 'or');
227
    }
228
229
    /**
230
     * Filter records where the given dimension exists in the given list of items
231
     *
232
     * @param string         $dimension  The dimension which you want to filter
233
     * @param string[]|int[] $items      A list of values. We will return records where the dimension is in this list.
234
     * @param \Closure|null  $extraction An extraction function to extract a different value from the dimension.
235
     * @param string         $boolean    This influences how this filter will be joined with previous added filters.
236
     *                                   Should both filters apply ("and") or one or the other ("or") ? Default is
237
     *                                   "and".
238
     *
239
     * @return $this
240
     */
241 3
    public function whereIn(string $dimension, array $items, Closure $extraction = null, string $boolean = 'and'): self
242
    {
243 3
        $filter = new InFilter($dimension, $items, $this->getExtraction($extraction));
244
245 3
        return $this->where($filter, null, null, null, $boolean);
246
    }
247
248
    /**
249
     * The expression filter allows for the implementation of arbitrary conditions, leveraging the Druid expression
250
     * system.
251
     *
252
     * This filter allows for more flexibility, but it might be less performant than a combination of the other filters
253
     * on this page due to the fact that not all filter optimizations are in place yet.
254
     *
255
     * @param string $expression        The expression to filter on
256
     * @param string $boolean           This influences how this filter will be joined with previous added filters.
257
     *                                  Should both filters apply ("and") or one or the other ("or") ? Default is
258
     *                                  "and".
259
     *
260
     * @return $this
261
     * @see https://druid.apache.org/docs/latest/misc/math-expr.html
262
     */
263 3
    public function whereExpression(string $expression, string $boolean = 'and'): self
264
    {
265 3
        $filter = new ExpressionFilter($expression);
266
267 3
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
268
269 3
        return $this;
270
    }
271
272
    /**
273
     * The expression filter allows for the implementation of arbitrary conditions, leveraging the Druid expression
274
     * system.
275
     *
276
     * This filter allows for more flexibility, but it might be less performant than a combination of the other filters
277
     * on this page due to the fact that not all filter optimizations are in place yet.
278
     *
279
     * @param string $expression
280
     *
281
     * @return $this
282
     */
283 1
    public function orWhereExpression(string $expression): self
284
    {
285 1
        return $this->whereExpression($expression, 'or');
286
    }
287
288
    /**
289
     * Filter records where the given dimension exists in the given list of items.
290
     *
291
     * If there are previously defined filters, this filter will be joined with an "or".
292
     *
293
     * @param string         $dimension  The dimension which you want to filter
294
     * @param string[]|int[] $items      A list of values. We will return records where the dimension is in this list.
295
     * @param \Closure|null  $extraction An extraction function to extract a different value from the dimension.
296
     *
297
     * @return $this
298
     */
299 1
    public function orWhereIn(string $dimension, array $items, Closure $extraction = null): self
300
    {
301 1
        return $this->whereIn($dimension, $items, $extraction, 'or');
302
    }
303
304
    /**
305
     * Filter records where dimensionA is equal to dimensionB.
306
     * You can either supply a string or a Closure. The Closure will receive a DimensionBuilder object, which allows
307
     * you to select a dimension and apply extraction functions if needed.
308
     *
309
     * Example:
310
     * ```php
311
     * $builder->whereColumn('initials', function(DimensionBuilder $dimensionBuilder) {
312
     *   $dimensionBuilder->select('first_name', function(ExtractionBuilder $extractionBuilder) {
313
     *     $extractionBuilder->substring(0, 1);
314
     *   });
315
     * });
316
     * ```
317
     *
318
     * @param Closure|string $dimensionA The dimension which you want to compare, or a Closure which will receive a
319
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
320
     * @param Closure|string $dimensionB The dimension which you want to compare, or a Closure which will receive a
321
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
322
     * @param string         $boolean    This influences how this filter will be joined with previous added filters.
323
     *                                   Should both filters apply ("and") or one or the other ("or") ? Default is
324
     *                                   "and".
325
     *
326
     * @return $this
327
     */
328 2
    public function whereColumn(Closure|string $dimensionA, Closure|string $dimensionB, string $boolean = 'and'): self
329
    {
330 2
        $filter = new ColumnComparisonFilter(
331 2
            $this->columnCompareDimension($dimensionA),
332 2
            $this->columnCompareDimension($dimensionB)
333 2
        );
334
335 2
        return $this->where($filter, null, null, null, $boolean);
336
    }
337
338
    /**
339
     * Filter records where dimensionA is equal to dimensionB.
340
     * You can either supply a string or a Closure. The Closure will receive a DimensionBuilder object, which allows
341
     * you to select a dimension and apply extraction functions if needed.
342
     *
343
     * Example:
344
     * ```php
345
     * $builder->orWhereColumn('initials', function(DimensionBuilder $dimensionBuilder) {
346
     *   $dimensionBuilder->select('first_name', function(ExtractionBuilder $extractionBuilder) {
347
     *     $extractionBuilder->substring(0, 1);
348
     *   });
349
     * });
350
     * ```
351
     *
352
     * @param Closure|string $dimensionA The dimension which you want to compare, or a Closure which will receive a
353
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
354
     * @param Closure|string $dimensionB The dimension which you want to compare, or a Closure which will receive a
355
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
356
     *
357
     * @return $this
358
     */
359 1
    public function orWhereColumn(Closure|string $dimensionA, Closure|string $dimensionB): self
360
    {
361 1
        return $this->whereColumn($dimensionA, $dimensionB, 'or');
362
    }
363
364
    /**
365
     * Filter records where dimensionA is NOT equal to dimensionB.
366
     * You can either supply a string or a Closure. The Closure will receive a DimensionBuilder object, which allows
367
     * you to select a dimension and apply extraction functions if needed.
368
     *
369
     * Example:
370
     * ```php
371
     * $builder->orWhereNotColumn('initials', function(DimensionBuilder $dimensionBuilder) {
372
     *   $dimensionBuilder->select('first_name', function(ExtractionBuilder $extractionBuilder) {
373
     *     $extractionBuilder->substring(0, 1);
374
     *   });
375
     * });
376
     * ```
377
     *
378
     * @param Closure|string $dimensionA The dimension which you want to compare, or a Closure which will receive a
379
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
380
     * @param Closure|string $dimensionB The dimension which you want to compare, or a Closure which will receive a
381
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
382
     *
383
     * @return $this
384
     */
385 1
    public function orWhereNotColumn(Closure|string $dimensionA, Closure|string $dimensionB): self
386
    {
387 1
        return $this->whereNot(function (FilterBuilder $builder) use ($dimensionA, $dimensionB) {
388 1
            $builder->whereColumn($dimensionA, $dimensionB);
389 1
        }, 'or');
390
    }
391
392
    /**
393
     * This filter will select records where the given dimension is greater than or equal to the given minValue, and
394
     * less than or equal to the given $maxValue.
395
     *
396
     * So in SQL syntax, this would be:
397
     * ```
398
     * WHERE dimension => $minValue AND dimension <= $maxValue
399
     * ```
400
     *
401
     * @param string                   $dimension  The dimension which you want to filter
402
     * @param int|string               $minValue   The minimum value where the dimension should match. It should be
403
     *                                             equal or greater than this value.
404
     * @param int|string               $maxValue   The maximum value where the dimension should match. It should be
405
     *                                             less than this value.
406
     * @param \Closure|null            $extraction Extraction function to extract a different value from the dimension.
407
     * @param null|string|SortingOrder $ordering   Specifies the sorting order using when comparing values against the
408
     *                                             between filter. Can be one of the following values: "lexicographic",
409
     *                                             "alphanumeric", "numeric", "strlen", "version". See Sorting Orders
410
     *                                             for more details. By default, it will be "numeric" if the values are
411
     *                                             numeric, otherwise it will be "lexicographic"
412
     * @param string                   $boolean    This influences how this filter will be joined with previous added
413
     *                                             filters. Should both filters apply ("and") or one or the other
414
     *                                             ("or") ? Default is
415
     *                                             "and".
416
     *
417
     * @return $this
418
     */
419 1
    public function whereBetween(
420
        string $dimension,
421
        int|string $minValue,
422
        int|string $maxValue,
423
        Closure $extraction = null,
424
        string|SortingOrder $ordering = null,
425
        string $boolean = 'and'
426
    ): self {
427 1
        $filter = new BetweenFilter($dimension, $minValue, $maxValue, $ordering, $this->getExtraction($extraction));
428
429 1
        return $this->where($filter, null, null, null, $boolean);
430
    }
431
432
    /**
433
     * This filter will select records where the given dimension is greater than or equal to the given minValue, and
434
     * less than or equal to the given $maxValue.
435
     *
436
     * This method will join previous added filters with an "or" instead of an "and".
437
     *
438
     * So in SQL syntax, this would be:
439
     * ```
440
     * WHERE (dimension => $minValue AND dimension <= $maxValue) or .... (other filters here)
441
     * ```
442
     *
443
     * @param string        $dimension   The dimension which you want to filter
444
     * @param int|string    $minValue    The minimum value where the dimension should match. It should be equal or
445
     *                                   greater than this value.
446
     * @param int|string    $maxValue    The maximum value where the dimension should match. It should be less than
447
     *                                   this value.
448
     * @param \Closure|null $extraction  Extraction function to extract a different value from the dimension.
449
     * @param null|string|SortingOrder   $ordering    Specifies the sorting order using when comparing values against the
450
     *                                   between filter. Can be one of the following values: "lexicographic",
451
     *                                   "alphanumeric", "numeric", "strlen", "version". See Sorting Orders for
452
     *                                   more details. By default, it will be "numeric" if the values are
453
     *                                   numeric, otherwise it will be "lexicographic"
454
     *
455
     * @return $this
456
     */
457 1
    public function orWhereBetween(
458
        string $dimension,
459
        int|string $minValue,
460
        int|string $maxValue,
461
        Closure $extraction = null,
462
        string|SortingOrder $ordering = null
463
    ): self {
464 1
        return $this->whereBetween($dimension, $minValue, $maxValue, $extraction, $ordering, 'or');
465
    }
466
467
    /**
468
     * Filter on records which match using a bitwise AND comparison.
469
     *
470
     * Only records will match where the dimension contains ALL bits which are also enabled in the given $flags
471
     * argument. Support for 64-bit integers are supported.
472
     *
473
     * Druid has support for bitwise flags since version 0.20.2.
474
     * Before that, we have built our own variant, but then javascript support is required. If this is the case, set
475
     * $useJavascript to true.
476
     *
477
     * JavaScript-based functionality is disabled by default. Please refer to the Druid JavaScript programming guide
478
     * for guidelines about using Druid's JavaScript functionality, including instructions on how to enable it:
479
     * https://druid.apache.org/docs/latest/development/javascript.html
480
     *
481
     * @param string $dimension     The dimension which contains int values where you want to do a bitwise AND check
482
     *                              against.
483
     * @param int    $flags         The bits which you want to check if they are enabled in the given dimension.
484
     * @param bool   $useJavascript When set to true, we will use the javascript variant instead of the bitwiseAnd
485
     *                              expression. See above for more information.
486
     *
487
     * @return $this
488
     */
489 1
    public function orWhereFlags(string $dimension, int $flags, bool $useJavascript = false): self
490
    {
491 1
        return $this->whereFlags($dimension, $flags, 'or', $useJavascript);
492
    }
493
494
    /**
495
     * Filter on records which match using a bitwise AND comparison.
496
     *
497
     * Only records will match where the dimension contains ALL bits which are also enabled in the given $flags
498
     * argument. Support for 64-bit integers are supported.
499
     *
500
     * Druid has support for bitwise flags since version 0.20.2.
501
     * Before that, we have built our own variant, but then javascript support is required. If this is the case, set
502
     * $useJavascript to true.
503
     *
504
     * JavaScript-based functionality is disabled by default. Please refer to the Druid JavaScript programming guide
505
     * for guidelines about using Druid's JavaScript functionality, including instructions on how to enable it:
506
     * https://druid.apache.org/docs/latest/development/javascript.html
507
     *
508
     * @param string $dimension     The dimension which contains int values where you want to do a bitwise AND check
509
     *                              against.
510
     * @param int    $flags         The bits which you want to check if they are enabled in the given dimension.
511
     * @param string $boolean       This influences how this filter will be joined with previous added filters. Should
512
     *                              both filters apply ("and") or one or the other ("or") ? Default is "and".
513
     * @param bool   $useJavascript When set to true, we will use the javascript variant instead of the bitwiseAnd
514
     *                              expression. See above.
515
     *
516
     * @return $this
517
     * @throws \BadFunctionCallException
518
     */
519 5
    public function whereFlags(
520
        string $dimension,
521
        int $flags,
522
        string $boolean = 'and',
523
        bool $useJavascript = false
524
    ): self {
525
        // Older versions of druid do not have the bitwiseAnd expression yet. Therefore, you can use a javascript variant
526
        // as alternative.
527 5
        if ($useJavascript) {
528 1
            return $this->where($dimension, '=', $flags, function (ExtractionBuilder $extraction) use ($flags) {
529
                // Do a binary "AND" flag comparison on a 64 bit int. The result will either be the
530
                // $flags, or 0 when it's bit is not set.
531 1
                $extraction->javascript('
532
                    function(dimensionValue) { 
533 1
                        var givenValue = ' . $flags . '; 
534
                        var hi = 0x80000000; 
535
                        var low = 0x7fffffff; 
536
                        var hi1 = ~~(dimensionValue / hi); 
537
                        var hi2 = ~~(givenValue / hi); 
538
                        var low1 = dimensionValue & low; 
539
                        var low2 = givenValue & low; 
540
                        var h = hi1 & hi2; 
541
                        var l = low1 & low2; 
542
                        return (h*hi + l); 
543
                    }
544 1
                ');
545 1
            }, $boolean);
546
        }
547
548
        // If we do not have access to a query builder object, we cannot select our
549
        // flags value as a virtual column. This situation can happen for example when
550
        // we are in a task-builder. In that case, we will use the expression filter.
551 4
        if (!$this->query instanceof QueryBuilder) {
552 2
            return $this->whereExpression('bitwiseAnd("' . $dimension . '", ' . $flags . ') == ' . $flags);
553
        }
554
555 2
        $placeholder                 = 'v' . count($this->query->placeholders);
556 2
        $this->query->placeholders[] = $placeholder;
557
558 2
        $this->query->virtualColumn(
559 2
            'bitwiseAnd("' . $dimension . '", ' . $flags . ')',
560 2
            $placeholder,
561 2
            DataType::LONG
562 2
        );
563
564 2
        return $this->where($placeholder, '=', $flags, null, $boolean);
565
    }
566
567
    /**
568
     * Filter on a dimension where the value exists in the given intervals array.
569
     *
570
     * The intervals array can contain the following:
571
     * - an Interval object
572
     * - a raw interval string as used in druid. For example: 2019-04-15T08:00:00.000Z/2019-04-15T09:00:00.000Z
573
     * - an array which contains 2 elements, a start and stop date. These can be an DateTime object, a unix timestamp
574
     *   or anything which can be parsed by DateTime::__construct
575
     *
576
     * @param string                                                               $dimension  The dimension which you
577
     *                                                                                         want to filter
578
     * @param array<string|IntervalInterface|array<string|\DateTimeInterface|int>> $intervals  The interval which you
579
     *                                                                                         want to match. See above
580
     *                                                                                         for more info.
581
     * @param \Closure|null                                                        $extraction Extraction function to
582
     *                                                                                         extract a different
583
     *                                                                                         value from the
584
     *                                                                                         dimension.
585
     * @param string                                                               $boolean    This influences how this
586
     *                                                                                         filter will be joined
587
     *                                                                                         with previous added
588
     *                                                                                         filters. Should both
589
     *                                                                                         filters apply
590
     *                                                                                         ("and") or one or the
591
     *                                                                                         other ("or") ? Default
592
     *                                                                                         is
593
     *                                                                                         "and".
594
     *
595
     * @return $this
596
     * @throws \Exception
597
     */
598 1
    public function whereInterval(
599
        string $dimension,
600
        array $intervals,
601
        Closure $extraction = null,
602
        string $boolean = 'and'
603
    ): self {
604 1
        $filter = new IntervalFilter(
605 1
            $dimension,
606 1
            $this->normalizeIntervals($intervals),
607 1
            $this->getExtraction($extraction)
608 1
        );
609
610 1
        return $this->where($filter, null, null, null, $boolean);
611
    }
612
613
    /**
614
     * Filter on a dimension where the value exists in the given intervals array.
615
     *
616
     * The intervals array can contain the following:
617
     * - an Interval object
618
     * - a raw interval string as used in druid. For example: 2019-04-15T08:00:00.000Z/2019-04-15T09:00:00.000Z
619
     * - an array which contains 2 elements, a start and stop date. These can be an DateTime object, a unix timestamp
620
     *   or anything which can be parsed by DateTime::__construct
621
     *
622
     * @param string                                                               $dimension  The dimension which you
623
     *                                                                                         want to filter
624
     * @param array<string|IntervalInterface|array<string|\DateTimeInterface|int>> $intervals  The interval which you
625
     *                                                                                         want to match. See above
626
     *                                                                                         for more info.
627
     * @param \Closure|null                                                        $extraction Extraction function to
628
     *                                                                                         extract a different
629
     *                                                                                         value from the
630
     *                                                                                         dimension.
631
     *
632
     * @return $this
633
     * @throws \Exception
634
     */
635 1
    public function orWhereInterval(string $dimension, array $intervals, Closure $extraction = null): self
636
    {
637 1
        return $this->whereInterval($dimension, $intervals, $extraction, 'or');
638
    }
639
640
    /**
641
     * Filter on a spatial dimension where the spatial dimension value (x,y coordinates) are between the
642
     * given min and max coordinates.
643
     *
644
     * @param string  $dimension The name of the spatial dimension.
645
     * @param float[] $minCoords List of minimum dimension coordinates for coordinates [x, y, z, …]
646
     * @param float[] $maxCoords List of maximum dimension coordinates for coordinates [x, y, z, …]
647
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
648
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
649
     *                           "and".
650
     *
651
     * @return $this
652
     */
653 2
    public function whereSpatialRectangular(
654
        string $dimension,
655
        array $minCoords,
656
        array $maxCoords,
657
        string $boolean = 'and'
658
    ): self {
659 2
        $filter = new SpatialRectangularFilter($dimension, $minCoords, $maxCoords);
660
661 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
662
663 2
        return $this;
664
    }
665
666
    /**
667
     * Select all records where the spatial dimension is within the given radios.
668
     * You can specify an x,y, z coordinate and the radius. All the records where the spatial dimension
669
     * is within the given area will be returned.
670
     *
671
     * @param string  $dimension The name of the spatial dimension.
672
     * @param float[] $coords    Origin coordinates in the form [x, y, z, …]
673
     * @param float   $radius    The float radius value
674
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
675
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
676
     *                           "and".
677
     *
678
     * @return $this
679
     */
680 2
    public function whereSpatialRadius(string $dimension, array $coords, float $radius, string $boolean = 'and'): self
681
    {
682 2
        $filter = new SpatialRadiusFilter($dimension, $coords, $radius);
683
684 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
685
686 2
        return $this;
687
    }
688
689
    /**
690
     * Return the records where the spatial dimension is within the area of the defined polygon.
691
     *
692
     * @param string  $dimension The name of the spatial dimension.
693
     * @param float[] $abscissa  (The x-axis) Horizontal coordinate for corners of the polygon
694
     * @param float[] $ordinate  (The y-axis) Vertical coordinate for corners of the polygon
695
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
696
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
697
     *                           "and".
698
     *
699
     * @return $this
700
     */
701 2
    public function whereSpatialPolygon(
702
        string $dimension,
703
        array $abscissa,
704
        array $ordinate,
705
        string $boolean = 'and'
706
    ): self {
707 2
        $filter = new SpatialPolygonFilter($dimension, $abscissa, $ordinate);
708
709 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
710
711 2
        return $this;
712
    }
713
714
    /**
715
     * Filter on a spatial dimension where the spatial dimension value (x,y coordinates) are between the
716
     * given min and max coordinates.
717
     *
718
     * @param string  $dimension The name of the spatial dimension.
719
     * @param float[] $minCoords List of minimum dimension coordinates for coordinates [x, y, z, …]
720
     * @param float[] $maxCoords List of maximum dimension coordinates for coordinates [x, y, z, …]
721
     *
722
     * @return $this
723
     */
724 1
    public function orWhereSpatialRectangular(string $dimension, array $minCoords, array $maxCoords): self
725
    {
726 1
        return $this->whereSpatialRectangular($dimension, $minCoords, $maxCoords, 'or');
727
    }
728
729
    /**
730
     * Select all records where the spatial dimension is within the given radios.
731
     * You can specify an x,y, z coordinate and the radius. All the records where the spatial dimension
732
     * is within the given area will be returned.
733
     *
734
     * @param string  $dimension The name of the spatial dimension.
735
     * @param float[] $coords    Origin coordinates in the form [x, y, z, …]
736
     * @param float   $radius    The float radius value
737
     *
738
     * @return $this
739
     */
740 1
    public function orWhereSpatialRadius(string $dimension, array $coords, float $radius): self
741
    {
742 1
        return $this->whereSpatialRadius($dimension, $coords, $radius, 'or');
743
    }
744
745
    /**
746
     * Return the records where the spatial dimension is within the area of the defined polygon.
747
     *
748
     * @param string  $dimension The name of the spatial dimension.
749
     * @param float[] $abscissa  (The x-axis) Horizontal coordinate for corners of the polygon
750
     * @param float[] $ordinate  (The y-axis) Vertical coordinate for corners of the polygon
751
     *
752
     * @return $this
753
     */
754 1
    public function orWhereSpatialPolygon(string $dimension, array $abscissa, array $ordinate): self
755
    {
756 1
        return $this->whereSpatialPolygon($dimension, $abscissa, $ordinate, 'or');
757
    }
758
759
    /**
760
     * Normalize the given dimension to a DimensionInterface object.
761
     *
762
     * @param Closure|string $dimension
763
     *
764
     * @return \Level23\Druid\Dimensions\DimensionInterface
765
     * @throws InvalidArgumentException
766
     */
767 5
    protected function columnCompareDimension(Closure|string $dimension): DimensionInterface
768
    {
769 5
        if ($dimension instanceof Closure) {
770 2
            $builder = new DimensionBuilder();
771 2
            call_user_func($dimension, $builder);
772 2
            $dimensions = $builder->getDimensions();
773
774 2
            if (count($dimensions) != 1) {
775 1
                throw new InvalidArgumentException('Your dimension builder should select 1 dimension');
776
            }
777
778 1
            return $dimensions[0];
779
        }
780
781 3
        return new Dimension($dimension);
782
    }
783
784
    /**
785
     * Normalize the given intervals into Interval objects.
786
     *
787
     * @param array<string|IntervalInterface|array<string|\DateTimeInterface|int>> $intervals
788
     *
789
     * @return array<IntervalInterface>
790
     * @throws \Exception
791
     */
792 13
    protected function normalizeIntervals(array $intervals): array
793
    {
794 13
        if (sizeof($intervals) == 0) {
795 1
            return [];
796
        }
797
798 12
        $first = reset($intervals);
799
800
        // If first is an array or already a druid interval string or object we do not wrap it in an array
801 12
        if (!is_array($first) && !$this->isDruidInterval($first)) {
802 8
            $intervals = [$intervals];
803
        }
804
805 12
        return array_map(function ($interval) {
806
807
            /** @var string|IntervalInterface|array<string|\DateTimeInterface|int> $interval */
808 12
            if ($interval instanceof IntervalInterface) {
809 1
                return $interval;
810
            }
811
812
            // If it is a string we explode it into to elements
813 11
            if (is_string($interval)) {
814 2
                $interval = explode('/', $interval);
815
            }
816
817
            // If the value is an array and is not empty and has either one or 2 values it's an interval array
818 11
            if (is_array($interval) && !empty(array_filter($interval)) && count($interval) < 3) {
819
                /** @scrutinizer ignore-type */
820 6
                return new Interval(...$interval);
821
            }
822
823 5
            throw new InvalidArgumentException(
824 5
                'Invalid type given in the interval array. We cannot process ' .
825 5
                var_export($interval, true)
826 5
            );
827 12
        }, $intervals);
828
    }
829
830
    /**
831
     * Returns true if the argument provided is a druid interval string or interface
832
     *
833
     * @param mixed $interval
834
     *
835
     * @return bool
836
     */
837 11
    protected function isDruidInterval(mixed $interval): bool
838
    {
839 11
        if ($interval instanceof IntervalInterface) {
840 1
            return true;
841
        }
842
843 10
        return is_string($interval) && str_contains($interval, '/');
844
    }
845
846
    /**
847
     * Helper method to add an OR filter
848
     *
849
     * @param FilterInterface $filter
850
     */
851 7
    protected function addOrFilter(FilterInterface $filter): void
852
    {
853 7
        if (!$this->filter instanceof FilterInterface) {
854 5
            $this->filter = $filter;
855
856 5
            return;
857
        }
858
859 7
        if ($this->filter instanceof OrFilter) {
860 1
            $this->filter->addFilter($filter);
0 ignored issues
show
Bug introduced by
The method addFilter() does not exist on Level23\Druid\Filters\FilterInterface. It seems like you code against a sub-type of Level23\Druid\Filters\FilterInterface such as Level23\Druid\Filters\OrFilter or Level23\Druid\Filters\AndFilter. ( Ignorable by Annotation )

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

860
            $this->filter->/** @scrutinizer ignore-call */ 
861
                           addFilter($filter);
Loading history...
861
862 1
            return;
863
        }
864
865 7
        $this->filter = new OrFilter([$this->filter, $filter]);
866
    }
867
868
    /**
869
     * Helper method to add an AND filter
870
     *
871
     * @param FilterInterface $filter
872
     */
873 51
    protected function addAndFilter(FilterInterface $filter): void
874
    {
875 51
        if (!$this->filter instanceof FilterInterface) {
876 51
            $this->filter = $filter;
877
878 51
            return;
879
        }
880
881 25
        if ($this->filter instanceof AndFilter) {
882 2
            $this->filter->addFilter($filter);
883
884 2
            return;
885
        }
886
887 25
        $this->filter = new AndFilter([$this->filter, $filter]);
888
    }
889
890
    /**
891
     * @return \Level23\Druid\Filters\FilterInterface|null
892
     */
893 37
    public function getFilter(): ?FilterInterface
894
    {
895 37
        return $this->filter;
896
    }
897
898
    /**
899
     * @param \Closure|null $extraction
900
     *
901
     * @return \Level23\Druid\Extractions\ExtractionInterface|null
902
     */
903 49
    private function getExtraction(?Closure $extraction): ?ExtractionInterface
904
    {
905 49
        if (empty($extraction)) {
906 48
            return null;
907
        }
908
909 1
        $builder = new ExtractionBuilder();
910 1
        call_user_func($extraction, $builder);
911
912 1
        return $builder->getExtraction();
913
    }
914
}