Completed
Push — master ( b809e7...b21e0c )
by Teye
08:09
created

HasFilter::virtualColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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