Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
164 / 164
100.00% covered (success)
100.00%
51 / 51
CRAP
100.00% covered (success)
100.00%
1 / 1
Where
100.00% covered (success)
100.00%
164 / 164
100.00% covered (success)
100.00%
51 / 51
75
100.00% covered (success)
100.00%
1 / 1
 where
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhere
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereNotEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereNotEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereNullSafeEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereNullSafeEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereLessThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereLessThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereLessThanOrEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereLessThanOrEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereGreaterThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereGreaterThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereGreaterThanOrEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereGreaterThanOrEqual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereLike
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereLike
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereNotLike
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereNotLike
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereNotIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereNotIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereNotBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereNotBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereIsNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereIsNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereIsNotNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereIsNotNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereMatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereMatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereMatchWithQueryExpansion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereMatchWithQueryExpansion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 whereMatchInBooleanMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orWhereMatchInBooleanMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addWhere
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 renderMatch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 renderMatchColumns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 renderMatchExpression
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 renderWhere
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 renderWherePart
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 renderWhereOperator
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 renderWhereValues
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
5
 prepareWhereValues
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 renderWhereValuesPartComparator
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 renderWhereValuesPartIn
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 renderWhereValuesPartBetween
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 renderWhereValuesPartIsNull
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Database Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Database\Manipulation\Traits;
11
12use Closure;
13use InvalidArgumentException;
14
15/**
16 * Trait Where.
17 *
18 * @package database
19 */
20trait Where
21{
22    /**
23     * Appends an "AND $column $operator ...$values" condition in the WHERE clause.
24     *
25     * @param array<array<mixed>|Closure|string>|Closure|string $column Closure for a subquery,
26     * a string with the column name or an array with column names on WHERE MATCH clause
27     * @param string $operator
28     * @param array<array<mixed>|Closure|float|int|string|null>|Closure|float|int|string|null ...$values
29     *
30     * @return static
31     */
32    public function where(
33        array | Closure | string $column,
34        string $operator,
35        array | Closure | float | int | string | null ...$values
36    ) : static {
37        // @phpstan-ignore-next-line
38        return $this->addWhere('AND', $column, $operator, $values);
39    }
40
41    /**
42     * Appends a "OR $column $operator ...$values" condition in the WHERE clause.
43     *
44     * @param array<array<mixed>|Closure|string>|Closure|string $column Closure for a subquery,
45     * a string with the column name or an array with column names on WHERE MATCH clause
46     * @param string $operator
47     * @param array<array<mixed>|Closure|float|int|string|null>|Closure|float|int|string|null ...$values
48     *
49     * @return static
50     */
51    public function orWhere(
52        array | Closure | string $column,
53        string $operator,
54        array | Closure | float | int | string | null ...$values
55    ) : static {
56        // @phpstan-ignore-next-line
57        return $this->addWhere('OR', $column, $operator, $values);
58    }
59
60    /**
61     * Appends an "AND $column = $value" condition in the WHERE clause.
62     *
63     * @param Closure|string $column Closure for a subquery or a string with the column name
64     * @param Closure|float|int|string|null $value
65     *
66     * @see https://mariadb.com/kb/en/equal/
67     *
68     * @return static
69     */
70    public function whereEqual(
71        Closure | string $column,
72        Closure | float | int | string | null $value
73    ) : static {
74        return $this->where($column, '=', $value);
75    }
76
77    /**
78     * Appends a "OR $column = $value" condition in the WHERE clause.
79     *
80     * @param Closure|string $column Closure for a subquery or a string with the column name
81     * @param Closure|float|int|string|null $value
82     *
83     * @see https://mariadb.com/kb/en/equal/
84     *
85     * @return static
86     */
87    public function orWhereEqual(
88        Closure | string $column,
89        Closure | float | int | string | null $value
90    ) : static {
91        return $this->orWhere($column, '=', $value);
92    }
93
94    /**
95     * Appends an "AND $column != $value" condition in the WHERE clause.
96     *
97     * @param Closure|string $column Closure for a subquery or a string with the column name
98     * @param Closure|float|int|string|null $value
99     *
100     * @see https://mariadb.com/kb/en/not-equal/
101     *
102     * @return static
103     */
104    public function whereNotEqual(
105        Closure | string $column,
106        Closure | float | int | string | null $value
107    ) : static {
108        return $this->where($column, '!=', $value);
109    }
110
111    /**
112     * Appends a "OR $column != $value" condition in the WHERE clause.
113     *
114     * @param Closure|string $column Closure for a subquery or a string with the column name
115     * @param Closure|float|int|string|null $value
116     *
117     * @see https://mariadb.com/kb/en/not-equal/
118     *
119     * @return static
120     */
121    public function orWhereNotEqual(
122        Closure | string $column,
123        Closure | float | int | string | null $value
124    ) : static {
125        return $this->orWhere($column, '!=', $value);
126    }
127
128    /**
129     * Appends an "AND $column <=> $value" condition in the WHERE clause.
130     *
131     * @param Closure|string $column Closure for a subquery or a string with the column name
132     * @param Closure|float|int|string|null $value
133     *
134     * @see https://mariadb.com/kb/en/null-safe-equal/
135     *
136     * @return static
137     */
138    public function whereNullSafeEqual(
139        Closure | string $column,
140        Closure | float | int | string | null $value
141    ) : static {
142        return $this->where($column, '<=>', $value);
143    }
144
145    /**
146     * Appends a "OR $column <=> $value" condition in the WHERE clause.
147     *
148     * @param Closure|string $column Closure for a subquery or a string with the column name
149     * @param Closure|float|int|string|null $value
150     *
151     * @see https://mariadb.com/kb/en/null-safe-equal/
152     *
153     * @return static
154     */
155    public function orWhereNullSafeEqual(
156        Closure | string $column,
157        Closure | float | int | string | null $value
158    ) : static {
159        return $this->orWhere($column, '<=>', $value);
160    }
161
162    /**
163     * Appends an "AND $column < $value" condition in the WHERE clause.
164     *
165     * @param Closure|string $column Closure for a subquery or a string with the column name
166     * @param Closure|float|int|string|null $value
167     *
168     * @see https://mariadb.com/kb/en/less-than/
169     *
170     * @return static
171     */
172    public function whereLessThan(
173        Closure | string $column,
174        Closure | float | int | string | null $value
175    ) : static {
176        return $this->where($column, '<', $value);
177    }
178
179    /**
180     * Appends a "OR $column < $value" condition in the WHERE clause.
181     *
182     * @param Closure|string $column Closure for a subquery or a string with the column name
183     * @param Closure|float|int|string|null $value
184     *
185     * @see https://mariadb.com/kb/en/less-than/
186     *
187     * @return static
188     */
189    public function orWhereLessThan(
190        Closure | string $column,
191        Closure | float | int | string | null $value
192    ) : static {
193        return $this->orWhere($column, '<', $value);
194    }
195
196    /**
197     * Appends an "AND $column <= $value" condition in the WHERE clause.
198     *
199     * @param Closure|string $column Closure for a subquery or a string with the column name
200     * @param Closure|float|int|string|null $value
201     *
202     * @see https://mariadb.com/kb/en/less-than-or-equal/
203     *
204     * @return static
205     */
206    public function whereLessThanOrEqual(
207        Closure | string $column,
208        Closure | float | int | string | null $value
209    ) : static {
210        return $this->where($column, '<=', $value);
211    }
212
213    /**
214     * Appends a "OR $column <= $value" condition in the WHERE clause.
215     *
216     * @param Closure|string $column Closure for a subquery or a string with the column name
217     * @param Closure|float|int|string|null $value
218     *
219     * @see https://mariadb.com/kb/en/less-than-or-equal/
220     *
221     * @return static
222     */
223    public function orWhereLessThanOrEqual(
224        Closure | string $column,
225        Closure | float | int | string | null $value
226    ) : static {
227        return $this->orWhere($column, '<=', $value);
228    }
229
230    /**
231     * Appends an "AND $column > $value" condition in the WHERE clause.
232     *
233     * @param Closure|string $column Closure for a subquery or a string with the column name
234     * @param Closure|float|int|string|null $value
235     *
236     * @see https://mariadb.com/kb/en/greater-than/
237     *
238     * @return static
239     */
240    public function whereGreaterThan(
241        Closure | string $column,
242        Closure | float | int | string | null $value
243    ) : static {
244        return $this->where($column, '>', $value);
245    }
246
247    /**
248     * Appends a "OR $column > $value" condition in the WHERE clause.
249     *
250     * @param Closure|string $column Closure for a subquery or a string with the column name
251     * @param Closure|float|int|string|null $value
252     *
253     * @see https://mariadb.com/kb/en/greater-than/
254     *
255     * @return static
256     */
257    public function orWhereGreaterThan(
258        Closure | string $column,
259        Closure | float | int | string | null $value
260    ) : static {
261        return $this->orWhere($column, '>', $value);
262    }
263
264    /**
265     * Appends an "AND $column >= $value" condition in the WHERE clause.
266     *
267     * @param Closure|string $column Closure for a subquery or a string with the column name
268     * @param Closure|float|int|string|null $value
269     *
270     * @see https://mariadb.com/kb/en/greater-than-or-equal/
271     *
272     * @return static
273     */
274    public function whereGreaterThanOrEqual(
275        Closure | string $column,
276        Closure | float | int | string | null $value
277    ) : static {
278        return $this->where($column, '>=', $value);
279    }
280
281    /**
282     * Appends a "OR $column >= $value" condition in the WHERE clause.
283     *
284     * @param Closure|string $column Closure for a subquery or a string with the column name
285     * @param Closure|float|int|string|null $value
286     *
287     * @see https://mariadb.com/kb/en/greater-than-or-equal/
288     *
289     * @return static
290     */
291    public function orWhereGreaterThanOrEqual(
292        Closure | string $column,
293        Closure | float | int | string | null $value
294    ) : static {
295        return $this->orWhere($column, '>=', $value);
296    }
297
298    /**
299     * Appends an "AND $column LIKE $value" condition in the WHERE clause.
300     *
301     * @param Closure|string $column Closure for a subquery or a string with the column name
302     * @param Closure|float|int|string|null $value
303     *
304     * @see https://mariadb.com/kb/en/like/
305     *
306     * @return static
307     */
308    public function whereLike(Closure | string $column, Closure | float | int | string | null $value) : static
309    {
310        return $this->where($column, 'LIKE', $value);
311    }
312
313    /**
314     * Appends a "OR $column LIKE $value" condition in the WHERE clause.
315     *
316     * @param Closure|string $column Closure for a subquery or a string with the column name
317     * @param Closure|float|int|string|null $value
318     *
319     * @see https://mariadb.com/kb/en/like/
320     *
321     * @return static
322     */
323    public function orWhereLike(
324        Closure | string $column,
325        Closure | float | int | string | null $value
326    ) : static {
327        return $this->orWhere($column, 'LIKE', $value);
328    }
329
330    /**
331     * Appends an "AND $column NOT LIKE $value" condition.
332     *
333     * @param Closure|string $column Closure for a subquery or a string with the column name
334     * @param Closure|float|int|string|null $value
335     *
336     * @see https://mariadb.com/kb/en/not-like/
337     *
338     * @return static
339     */
340    public function whereNotLike(
341        Closure | string $column,
342        Closure | float | int | string | null $value
343    ) : static {
344        return $this->where($column, 'NOT LIKE', $value);
345    }
346
347    /**
348     * Appends a "OR $column NOT LIKE $value" condition in the WHERE clause.
349     *
350     * @param Closure|string $column Closure for a subquery or a string with the column name
351     * @param Closure|float|int|string|null $value
352     *
353     * @see https://mariadb.com/kb/en/not-like/
354     *
355     * @return static
356     */
357    public function orWhereNotLike(
358        Closure | string $column,
359        Closure | float | int | string | null $value
360    ) : static {
361        return $this->orWhere($column, 'NOT LIKE', $value);
362    }
363
364    /**
365     * Appends an "AND $column IN (...$values)" condition in the WHERE clause.
366     *
367     * @param Closure|string $column Closure for a subquery or a string with the column name
368     * @param Closure|float|int|string|null $value
369     * @param Closure|float|int|string|null ...$values
370     *
371     * @see https://mariadb.com/kb/en/in/
372     *
373     * @return static
374     */
375    public function whereIn(
376        Closure | string $column,
377        Closure | float | int | string | null $value,
378        Closure | float | int | string | null ...$values
379    ) : static {
380        return $this->where($column, 'IN', ...[$value, ...$values]);
381    }
382
383    /**
384     * Appends a "OR $column IN (...$values)" condition in the WHERE clause.
385     *
386     * @param Closure|string $column Closure for a subquery or a string with the column name
387     * @param Closure|float|int|string|null $value
388     * @param Closure|float|int|string|null ...$values
389     *
390     * @see https://mariadb.com/kb/en/in/
391     *
392     * @return static
393     */
394    public function orWhereIn(
395        Closure | string $column,
396        Closure | float | int | string | null $value,
397        Closure | float | int | string | null ...$values
398    ) : static {
399        return $this->orWhere($column, 'IN', ...[$value, ...$values]);
400    }
401
402    /**
403     * Appends an "AND $column NOT IN (...$values)" condition in the WHERE clause.
404     *
405     * @param Closure|string $column Closure for a subquery or a string with the column name
406     * @param Closure|float|int|string|null $value
407     * @param Closure|float|int|string|null ...$values
408     *
409     * @see https://mariadb.com/kb/en/not-in/
410     *
411     * @return static
412     */
413    public function whereNotIn(
414        Closure | string $column,
415        Closure | float | int | string | null $value,
416        Closure | float | int | string | null ...$values
417    ) : static {
418        return $this->where($column, 'NOT IN', ...[$value, ...$values]);
419    }
420
421    /**
422     * Appends a "OR $column NOT IN (...$values)" condition in the WHERE clause.
423     *
424     * @param Closure|string $column Closure for a subquery or a string with the column name
425     * @param Closure|float|int|string|null $value
426     * @param Closure|float|int|string|null ...$values
427     *
428     * @see https://mariadb.com/kb/en/not-in/
429     *
430     * @return static
431     */
432    public function orWhereNotIn(
433        Closure | string $column,
434        Closure | float | int | string | null $value,
435        Closure | float | int | string | null ...$values
436    ) : static {
437        return $this->orWhere($column, 'NOT IN', ...[$value, ...$values]);
438    }
439
440    /**
441     * Appends an "AND $column BETWEEN $min AND $max" condition in the WHERE clause.
442     *
443     * @param Closure|string $column Closure for a subquery or a string with the column name
444     * @param Closure|float|int|string|null $min
445     * @param Closure|float|int|string|null $max
446     *
447     * @see https://mariadb.com/kb/en/between-and/
448     *
449     * @return static
450     */
451    public function whereBetween(
452        Closure | string $column,
453        Closure | float | int | string | null $min,
454        Closure | float | int | string | null $max
455    ) : static {
456        return $this->where($column, 'BETWEEN', $min, $max);
457    }
458
459    /**
460     * Appends a "OR $column BETWEEN $min AND $max" condition in the WHERE clause.
461     *
462     * @param Closure|string $column Closure for a subquery or a string with the column name
463     * @param Closure|float|int|string|null $min
464     * @param Closure|float|int|string|null $max
465     *
466     * @see https://mariadb.com/kb/en/between-and/
467     *
468     * @return static
469     */
470    public function orWhereBetween(
471        Closure | string $column,
472        Closure | float | int | string | null $min,
473        Closure | float | int | string | null $max
474    ) : static {
475        return $this->orWhere($column, 'BETWEEN', $min, $max);
476    }
477
478    /**
479     * Appends an "AND $column NOT BETWEEN $min AND $max" condition in the WHERE clause.
480     *
481     * @param Closure|string $column Closure for a subquery or a string with the column name
482     * @param Closure|float|int|string|null $min
483     * @param Closure|float|int|string|null $max
484     *
485     * @see https://mariadb.com/kb/en/not-between/
486     *
487     * @return static
488     */
489    public function whereNotBetween(
490        Closure | string $column,
491        Closure | float | int | string | null $min,
492        Closure | float | int | string | null $max
493    ) : static {
494        return $this->where($column, 'NOT BETWEEN', $min, $max);
495    }
496
497    /**
498     * Appends a "OR $column NOT BETWEEN $min AND $max" condition in the WHERE clause.
499     *
500     * @param Closure|string $column Closure for a subquery or a string with the column name
501     * @param Closure|float|int|string|null $min
502     * @param Closure|float|int|string|null $max
503     *
504     * @see https://mariadb.com/kb/en/not-between/
505     *
506     * @return static
507     */
508    public function orWhereNotBetween(
509        Closure | string $column,
510        Closure | float | int | string | null $min,
511        Closure | float | int | string | null $max
512    ) : static {
513        return $this->orWhere($column, 'NOT BETWEEN', $min, $max);
514    }
515
516    /**
517     * Appends an "AND $column IS NULL" condition in the WHERE clause.
518     *
519     * @param Closure|string $column Closure for a subquery or a string with the column name
520     *
521     * @see https://mariadb.com/kb/en/is-null/
522     *
523     * @return static
524     */
525    public function whereIsNull(Closure | string $column) : static
526    {
527        return $this->where($column, 'IS NULL');
528    }
529
530    /**
531     * Appends a "OR $column IS NULL" condition in the WHERE clause.
532     *
533     * @param Closure|string $column Closure for a subquery or a string with the column name
534     *
535     * @see https://mariadb.com/kb/en/is-null/
536     *
537     * @return static
538     */
539    public function orWhereIsNull(Closure | string $column) : static
540    {
541        return $this->orWhere($column, 'IS NULL');
542    }
543
544    /**
545     * Appends an "AND $column IS NOT NULL" condition in the WHERE clause.
546     *
547     * @param Closure|string $column Closure for a subquery or a string with the column name
548     *
549     * @see https://mariadb.com/kb/en/is-not-null/
550     *
551     * @return static
552     */
553    public function whereIsNotNull(Closure | string $column) : static
554    {
555        return $this->where($column, 'IS NOT NULL');
556    }
557
558    /**
559     * Appends a "OR $column IS NOT NULL" condition in the WHERE clause.
560     *
561     * @param Closure|string $column Closure for a subquery or a string with the column name
562     *
563     * @see https://mariadb.com/kb/en/is-not-null/
564     *
565     * @return static
566     */
567    public function orWhereIsNotNull(Closure | string $column) : static
568    {
569        return $this->orWhere($column, 'IS NOT NULL');
570    }
571
572    /* TODO: https://mariadb.com/kb/en/subqueries-and-exists/
573     public function whereExists(Closure $subquery)
574    {
575        $this->subquery($subquery);
576    }
577
578    public function whereNotExists(Closure $subquery)
579    {
580        $this->subquery($subquery);
581    }*/
582
583    /**
584     * Appends an "AND MATCH (...$columns) AGAINST ($against IN NATURAL LANGUAGE MODE)" fulltext
585     * searching in the WHERE clause.
586     *
587     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
588     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
589     *
590     * @see https://mariadb.com/kb/en/full-text-index-overview/
591     * @see https://mariadb.com/kb/en/match-against/
592     *
593     * @return static
594     */
595    public function whereMatch(
596        array | Closure | string $columns,
597        array | Closure | string $against
598    ) : static {
599        return $this->where($columns, 'MATCH', $against);
600    }
601
602    /**
603     * Appends a "OR MATCH (...$columns) AGAINST ($against IN NATURAL LANGUAGE MODE)" fulltext
604     * searching in the WHERE clause.
605     *
606     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
607     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
608     *
609     * @see https://mariadb.com/kb/en/full-text-index-overview/
610     * @see https://mariadb.com/kb/en/match-against/
611     *
612     * @return static
613     */
614    public function orWhereMatch(
615        array | Closure | string $columns,
616        array | Closure | string $against
617    ) : static {
618        return $this->orWhere($columns, 'MATCH', $against);
619    }
620
621    /**
622     * Appends an "AND MATCH (...$columns) AGAINST ($against WITH QUERY EXPANSION)" fulltext
623     * searching in the WHERE clause.
624     *
625     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
626     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
627     *
628     * @see https://mariadb.com/kb/en/full-text-index-overview/
629     * @see https://mariadb.com/kb/en/match-against/
630     *
631     * @return static
632     */
633    public function whereMatchWithQueryExpansion(
634        array | Closure | string $columns,
635        array | Closure | string $against
636    ) : static {
637        return $this->where($columns, 'MATCH', $against, 'WITH QUERY EXPANSION');
638    }
639
640    /**
641     * Appends a "OR MATCH (...$columns) AGAINST ($against WITH QUERY EXPANSION)" fulltext
642     * searching in the WHERE clause.
643     *
644     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
645     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
646     *
647     * @see https://mariadb.com/kb/en/full-text-index-overview/
648     * @see https://mariadb.com/kb/en/match-against/
649     *
650     * @return static
651     */
652    public function orWhereMatchWithQueryExpansion(
653        array | Closure | string $columns,
654        array | Closure | string $against
655    ) : static {
656        return $this->orWhere($columns, 'MATCH', $against, 'WITH QUERY EXPANSION');
657    }
658
659    /**
660     * Appends an "AND MATCH (...$columns) AGAINST ($against IN BOOLEAN MODE)" fulltext searching in
661     * the WHERE clause.
662     *
663     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
664     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
665     *
666     * @see https://mariadb.com/kb/en/full-text-index-overview/
667     * @see https://mariadb.com/kb/en/match-against/
668     *
669     * @return static
670     */
671    public function whereMatchInBooleanMode(
672        array | Closure | string $columns,
673        array | Closure | string $against
674    ) : static {
675        return $this->where($columns, 'MATCH', $against, 'IN BOOLEAN MODE');
676    }
677
678    /**
679     * Appends a "OR MATCH (...$columns) AGAINST ($against IN BOOLEAN MODE)" fulltext searching in
680     * the WHERE clause.
681     *
682     * @param array<array<mixed>|Closure|string>|Closure|string $columns Columns to MATCH
683     * @param array<array<mixed>|Closure|string>|Closure|string $against AGAINST expression
684     *
685     * @see https://mariadb.com/kb/en/full-text-index-overview/
686     * @see https://mariadb.com/kb/en/match-against/
687     *
688     * @return static
689     */
690    public function orWhereMatchInBooleanMode(
691        array | Closure | string $columns,
692        array | Closure | string $against
693    ) : static {
694        return $this->orWhere($columns, 'MATCH', $against, 'IN BOOLEAN MODE');
695    }
696
697    /**
698     * Adds a WHERE (or HAVING) part.
699     *
700     * @param string $glue `AND` or `OR`
701     * @param array<array<mixed>|Closure|string>|Closure|string $column
702     * @param string $operator `=`, `<=>`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`,
703     * `NOT LIKE`, `IN`, `NOT IN`, `BETWEEN`, `NOT BETWEEN`, `IS NULL`, `IS NOT NULL` or `MATCH`
704     * @param array<Closure|float|int|string|null> $values Values used by the operator
705     * @param string $clause `where` or `having`
706     *
707     * @return static
708     */
709    private function addWhere(
710        string $glue,
711        array | Closure | string $column,
712        string $operator,
713        array $values,
714        string $clause = 'where'
715    ) : static {
716        $this->sql[$clause][] = [
717            'glue' => $glue,
718            'column' => $column,
719            'operator' => $operator,
720            'values' => $values,
721        ];
722        return $this;
723    }
724
725    /**
726     * Renders a MATCH AGAINST clause.
727     *
728     * @param array<Closure|string>|Closure|string $columns
729     * @param array<string>|Closure|string $expression
730     * @param string $modifier
731     *
732     * @return string
733     */
734    private function renderMatch(
735        array | Closure | string $columns,
736        array | Closure | string $expression,
737        string $modifier = ''
738    ) {
739        $columns = $this->renderMatchColumns($columns);
740        $expression = $this->renderMatchExpression($expression);
741        if ($modifier) {
742            $modifier = ' ' . $modifier;
743        }
744        return "MATCH ({$columns}) AGAINST ({$expression}{$modifier})";
745    }
746
747    /**
748     * @param array<Closure|string>|Closure|string $columns
749     *
750     * @return string
751     */
752    private function renderMatchColumns(array | Closure | string $columns) : string
753    {
754        if (\is_array($columns)) {
755            foreach ($columns as &$column) {
756                $column = $this->renderIdentifier($column);
757            }
758            unset($column);
759            // @phpstan-ignore-next-line
760            return \implode(', ', $columns);
761        }
762        if (\is_string($columns)) {
763            return $this->renderIdentifier($columns);
764        }
765        return $this->subquery($columns);
766    }
767
768    /**
769     * @param array<string>|Closure|string $expression
770     *
771     * @return float|int|string
772     */
773    private function renderMatchExpression(
774        array | Closure | string $expression
775    ) : float | int | string {
776        if (\is_array($expression)) {
777            $expression = \implode(', ', $expression);
778            return $this->database->quote($expression);
779        }
780        if (\is_string($expression)) {
781            return $this->database->quote($expression);
782        }
783        return $this->subquery($expression);
784    }
785
786    /**
787     * Renders the full WHERE (or HAVING) clause.
788     *
789     * @param string $clause `where` or `having`
790     *
791     * @return string|null The full clause or null if has not a clause
792     */
793    protected function renderWhere(string $clause = 'where') : ?string
794    {
795        if ( ! isset($this->sql[$clause])) {
796            return null;
797        }
798        $parts = $this->sql[$clause];
799        $condition = $this->renderWherePart($parts[0], true);
800        unset($parts[0]);
801        foreach ($parts as $part) {
802            $condition .= $this->renderWherePart($part);
803        }
804        $clause = \strtoupper($clause);
805        return " {$clause} {$condition}";
806    }
807
808    /**
809     * Renders a WHERE part. Like: `AND column IN('value1', 'value2')`.
810     *
811     * @param array<string,mixed> $part Keys: `glue`, `operator`, `column` and `values`
812     * @param bool $first Is the first part? Prepends the operator (`AND` or `OR`)
813     *
814     * @return string
815     */
816    private function renderWherePart(array $part, bool $first = false) : string
817    {
818        $condition = '';
819        if ($first === false) {
820            $condition .= " {$part['glue']} ";
821        }
822        if ($part['operator'] === 'MATCH') {
823            return $condition . $this->renderMatch(
824                $part['column'],
825                $part['values'][0],
826                $part['values'][1] ?? ''
827            );
828        }
829        $part['column'] = $this->renderIdentifier($part['column']);
830        $part['operator'] = $this->renderWhereOperator($part['operator']);
831        $part['values'] = $this->renderWhereValues($part['operator'], $part['values']);
832        $condition .= "{$part['column']} {$part['operator']}";
833        $condition .= $part['values'] === null ? '' : " {$part['values']}";
834        return $condition;
835    }
836
837    /**
838     * Renders and validates a comparison operator.
839     *
840     * @param string $operator `=`, `<=>`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`,
841     * `NOT LIKE`, `IN`, `NOT IN`, `BETWEEN`, `NOT BETWEEN`, `IS NULL` or `IS NOT NULL`
842     *
843     * @throws InvalidArgumentException for invalid comparison operator
844     *
845     * @return string The operator
846     */
847    private function renderWhereOperator(string $operator) : string
848    {
849        $result = \strtoupper($operator);
850        if (\in_array($result, [
851            '=',
852            '<=>',
853            '!=',
854            '<>',
855            '>',
856            '>=',
857            '<',
858            '<=',
859            'LIKE',
860            'NOT LIKE',
861            'IN',
862            'NOT IN',
863            'BETWEEN',
864            'NOT BETWEEN',
865            'IS NULL',
866            'IS NOT NULL',
867        ], true)) {
868            return $result;
869        }
870        throw new InvalidArgumentException("Invalid comparison operator: {$operator}");
871    }
872
873    /**
874     * Renders the values used by a comparison operator.
875     *
876     * @param string $operator `=`, `<=>`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`,
877     * `NOT LIKE`, `IN`, `NOT IN`, `BETWEEN`, `NOT BETWEEN`, `IS NULL` or `IS NOT NULL`
878     * @param array<Closure|float|int|string|null> $values
879     *
880     * @throws InvalidArgumentException for invalid comparison operator
881     *
882     * @return string|null The values used by the operator
883     */
884    private function renderWhereValues(string $operator, array $values) : ?string
885    {
886        $values = \array_values($values);
887        $values = $this->prepareWhereValues($values);
888        if (\in_array($operator, [
889            '=',
890            '<=>',
891            '!=',
892            '<>',
893            '>',
894            '>=',
895            '<',
896            '<=',
897            'LIKE',
898            'NOT LIKE',
899        ], true)) {
900            return $this->renderWhereValuesPartComparator($operator, $values);
901        }
902        if (\in_array($operator, [
903            'IN',
904            'NOT IN',
905        ], true)) {
906            return $this->renderWhereValuesPartIn($operator, $values);
907        }
908        if (\in_array($operator, [
909            'BETWEEN',
910            'NOT BETWEEN',
911        ], true)) {
912            return $this->renderWhereValuesPartBetween($operator, $values);
913        }
914        if (\in_array($operator, [
915            'IS NULL',
916            'IS NOT NULL',
917        ], true)) {
918            return $this->renderWhereValuesPartIsNull($operator, $values);
919        }
920        // @codeCoverageIgnoreStart
921        // Should never throw - renderWhereOperator runs before on renderWhere
922        throw new InvalidArgumentException("Invalid comparison operator: {$operator}");
923        // @codeCoverageIgnoreEnd
924    }
925
926    /**
927     * Quote the input values or transform it in subqueries.
928     *
929     * @param array<bool|Closure|float|int|string|null> $values
930     *
931     * @return array<float|int|string> Each input value quoted or transformed in subquery
932     */
933    private function prepareWhereValues(array $values) : array
934    {
935        foreach ($values as &$value) {
936            $value = $value instanceof Closure
937                ? $this->subquery($value)
938                : $this->database->quote($value);
939        }
940        // @phpstan-ignore-next-line
941        return $values;
942    }
943
944    /**
945     * Renders the values of operators that receive exactly one value.
946     *
947     * @param string $operator `=`, `<=>`,    `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE` or `NOT LIKE`
948     * @param array<float|int|string> $values Must receive exactly 1 value, index 0
949     *
950     * @throws InvalidArgumentException if $values has more than one value
951     *
952     * @return string
953     */
954    private function renderWhereValuesPartComparator(string $operator, array $values) : string
955    {
956        if (isset($values[1]) || ! isset($values[0])) {
957            throw new InvalidArgumentException(
958                "Operator {$operator} must receive exactly 1 parameter"
959            );
960        }
961        return (string) $values[0];
962    }
963
964    /**
965     * Implode values for `IN` or `NOT IN`.
966     *
967     * @param string $operator `IN` or `NOT IN`
968     * @param array<float|int|string> $values Must receive at least 1 value
969     *
970     * @throws InvalidArgumentException if $values does not receive any value
971     *
972     * @return string
973     */
974    private function renderWhereValuesPartIn(string $operator, array $values) : string
975    {
976        if (empty($values)) {
977            throw new InvalidArgumentException(
978                "Operator {$operator} must receive at least 1 parameter"
979            );
980        }
981        return '(' . \implode(', ', $values) . ')';
982    }
983
984    /**
985     * Renders values for `BETWEEN` or `NOT BETWEEN`.
986     *
987     * @param string $operator `BETWEEN` or `NOT BETWEEN`
988     * @param array<float|int|string> $values Two values, indexes 0 and 1
989     *
990     * @throws InvalidArgumentException if $values not receive exactly two values
991     *
992     * @return string
993     */
994    private function renderWhereValuesPartBetween(string $operator, array $values) : string
995    {
996        if (isset($values[2]) || ! isset($values[0], $values[1])) {
997            throw new InvalidArgumentException(
998                "Operator {$operator} must receive exactly 2 parameters"
999            );
1000        }
1001        return "{$values[0]} AND {$values[1]}";
1002    }
1003
1004    /**
1005     * Renders the lonely operators, `IS NULL` or `IS NOT NULL`.
1006     *
1007     * @param string $operator `IS NULL` or `IS NOT NULL`
1008     * @param array<float|int|string> $values Must be an empty array
1009     *
1010     * @throws InvalidArgumentException if $values is not empty
1011     *
1012     * @return null
1013     */
1014    private function renderWhereValuesPartIsNull(string $operator, array $values)
1015    {
1016        if ( ! empty($values)) {
1017            throw new InvalidArgumentException(
1018                "Operator {$operator} must not receive parameters"
1019            );
1020        }
1021        return null;
1022    }
1023}