Passed
Pull Request — master (#52)
by Teye
07:22 queued 03:03
created

HasFilter::whereNull()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

898
            $this->filter->/** @scrutinizer ignore-call */ 
899
                           addFilter($filter);
Loading history...
899
900 1
            return;
901
        }
902
903 7
        $this->filter = new OrFilter([$this->filter, $filter]);
904
    }
905
906
    /**
907
     * Helper method to add an AND filter
908
     *
909
     * @param FilterInterface $filter
910
     */
911 56
    protected function addAndFilter(FilterInterface $filter): void
912
    {
913 56
        if (!$this->filter instanceof FilterInterface) {
914 56
            $this->filter = $filter;
915
916 56
            return;
917
        }
918
919 29
        if ($this->filter instanceof AndFilter) {
920 2
            $this->filter->addFilter($filter);
921
922 2
            return;
923
        }
924
925 29
        $this->filter = new AndFilter([$this->filter, $filter]);
926
    }
927
928
    /**
929
     * @return \Level23\Druid\Filters\FilterInterface|null
930
     */
931 41
    public function getFilter(): ?FilterInterface
932
    {
933 41
        return $this->filter;
934
    }
935
936
    /**
937
     * @param \Closure|null $extraction
938
     *
939
     * @return \Level23\Druid\Extractions\ExtractionInterface|null
940
     */
941 53
    private function getExtraction(?Closure $extraction): ?ExtractionInterface
942
    {
943 53
        if (empty($extraction)) {
944 52
            return null;
945
        }
946
947 1
        $builder = new ExtractionBuilder();
948 1
        call_user_func($extraction, $builder);
949
950 1
        return $builder->getExtraction();
951
    }
952
}