Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.27% covered (warning)
80.27%
179 / 223
71.21% covered (warning)
71.21%
47 / 66
CRAP
0.00% covered (danger)
0.00%
0 / 1
Builder
80.27% covered (warning)
80.27%
179 / 223
71.21% covered (warning)
71.21%
47 / 66
236.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setConnection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 select
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 selectAs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 random
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 bind
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 toSql
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 resolveJoinDependencies
75.00% covered (warning)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
11.56
 raw
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isValidReadOnly
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
 distinct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 from
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 fromAs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 where
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 andWhere
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
 whereIn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 whereNull
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 whereNotNull
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 groupBy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 newest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 oldest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orderBy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 offset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 limit
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 union
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 lock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lockUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 find
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 min
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 max
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 avg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 insert
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 into
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 values
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 value
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 sets
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 delete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 increment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decrement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 join
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 leftJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 leftOuterJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 leftInnerJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rightJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rightOuterJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rightInnerJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outerJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 innerJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 crossJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fullJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fullOuterJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rollback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 orOn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 andOn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 merge
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getBindParamType
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getPublicColumnName
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\DataStorage\Database\Query
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Database\Query;
16
17use phpOMS\Algorithm\Graph\DependencyResolver;
18use phpOMS\Contract\SerializableInterface;
19use phpOMS\DataStorage\Database\BuilderAbstract;
20use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
21
22/**
23 * Database query builder.
24 *
25 * @package phpOMS\DataStorage\Database\Query
26 * @license OMS License 2.0
27 * @link    https://jingga.app
28 * @since   1.0.0
29 */
30class Builder extends BuilderAbstract
31{
32    /**
33     * Log queries.
34     *
35     * @var bool
36     * @since 1.0.0
37     */
38    public static bool $log = false;
39
40    /**
41     * Columns.
42     *
43     * @var array
44     * @since 1.0.0
45     */
46    public array $selects = [];
47
48    /**
49     * Columns.
50     *
51     * @var array
52     * @since 1.0.0
53     */
54    public array $random;
55
56    /**
57     * Columns.
58     *
59     * @var array
60     * @since 1.0.0
61     */
62    public array $updates = [];
63
64    /**
65     * Stupid work around because value needs to be not null for it to work in Grammar.
66     *
67     * @var array
68     * @since 1.0.0
69     */
70    public array $deletes = [1];
71
72    /**
73     * Into.
74     *
75     * @var string
76     * @since 1.0.0
77     */
78    public string $into = '';
79
80    /**
81     * Into columns.
82     *
83     * @var array
84     * @since 1.0.0
85     */
86    public array $inserts = [];
87
88    /**
89     * Into columns.
90     *
91     * @var array<int, mixed>
92     * @since 1.0.0
93     */
94    public array $values = [];
95
96    /**
97     * Into columns.
98     *
99     * @var array
100     * @since 1.0.0
101     */
102    public array $sets = [];
103
104    /**
105     * Distinct.
106     *
107     * @var bool
108     * @since 1.0.0
109     */
110    public bool $distinct = false;
111
112    /**
113     * From.
114     *
115     * @var array
116     * @since 1.0.0
117     */
118    public array $from = [];
119
120    /**
121     * Joins.
122     *
123     * @var array
124     * @since 1.0.0
125     */
126    public array $joins = [];
127
128    /**
129     * Ons of joins.
130     *
131     * @var array
132     * @since 1.0.0
133     */
134    public array $ons = [];
135
136    /**
137     * Where.
138     *
139     * @var array
140     * @since 1.0.0
141     */
142    public array $wheres = [];
143
144    /**
145     * Group.
146     *
147     * @var string[]|self[]
148     * @since 1.0.0
149     */
150    public array $groups = [];
151
152    /**
153     * Order.
154     *
155     * @var array
156     * @since 1.0.0
157     */
158    public array $orders = [];
159
160    /**
161     * Limit.
162     *
163     * @var null|int
164     * @since 1.0.0
165     */
166    public ?int $limit = null;
167
168    /**
169     * Offset.
170     *
171     * @var null|int
172     * @since 1.0.0
173     */
174    public ?int $offset = null;
175
176    /**
177     * Binds.
178     *
179     * @var array
180     * @since 1.0.0
181     */
182    private array $binds = [];
183
184    /**
185     * Union.
186     *
187     * @var array
188     * @since 1.0.0
189     */
190    public array $unions = [];
191
192    /**
193     * Lock.
194     *
195     * @var bool
196     * @since 1.0.0
197     */
198    public bool $lock = false;
199
200    /**
201     * Comparison OPERATORS.
202     *
203     * @var string[]
204     * @since 1.0.0
205     */
206    public const OPERATORS = [
207        '=',
208        '<',
209        '>',
210        '<=',
211        '>=',
212        '<>',
213        '!=',
214        'like',
215        'like binary',
216        'not like',
217        'between',
218        'ilike',
219        '&',
220        '|',
221        '^',
222        '<<',
223        '>>',
224        'rlike',
225        'regexp',
226        'not regexp',
227        '~',
228        '~*',
229        '!~',
230        '!~*',
231        'similar to',
232        'not similar to',
233        'in',
234    ];
235
236    /**
237     * Constructor.
238     *
239     * @param ConnectionAbstract $connection Database connection
240     * @param bool               $readOnly   Query is read only
241     *
242     * @since 1.0.0
243     */
244    public function __construct(ConnectionAbstract $connection, bool $readOnly = false)
245    {
246        $this->isReadOnly = $readOnly;
247        $this->setConnection($connection);
248    }
249
250    /**
251     * Set connection for grammar.
252     *
253     * @param ConnectionAbstract $connection Database connection
254     *
255     * @return void
256     *
257     * @since 1.0.0
258     */
259    public function setConnection(ConnectionAbstract $connection) : void
260    {
261        $this->connection = $connection;
262        $this->grammar    = $connection->getGrammar();
263    }
264
265    /**
266     * Select.
267     *
268     * @param mixed ...$columns Columns
269     *
270     * @return Builder
271     *
272     * @throws \InvalidArgumentException
273     *
274     * @since 1.0.0
275     */
276    public function select(mixed ...$columns) : self
277    {
278        $this->type = QueryType::SELECT;
279
280        /** @var mixed[] $columns */
281        /** @var mixed $column */
282        foreach ($columns as $column) {
283            if (\is_string($column) || $column instanceof self) {
284                $this->selects[] = $column;
285            } else {
286                throw new \InvalidArgumentException();
287            }
288        }
289
290        return $this;
291    }
292
293    /**
294     * Select with alias.
295     *
296     * @param mixed  $column Column query
297     * @param string $alias  Alias
298     *
299     * @return Builder
300     *
301     * @since 1.0.0
302     */
303    public function selectAs(mixed $column, string $alias) : self
304    {
305        $this->type            = QueryType::SELECT;
306        $this->selects[$alias] = $column;
307
308        return $this;
309    }
310
311    /**
312     * Select.
313     *
314     * @param array ...$columns Columns
315     *
316     * @return Builder
317     *
318     * @since 1.0.0
319     */
320    public function random(mixed ...$columns) : self
321    {
322        $this->select(...$columns);
323
324        $this->type   = QueryType::RANDOM;
325        $this->random = &$this->selects;
326
327        return $this;
328    }
329
330    /**
331     * Bind parameter.
332     *
333     * @param string|array $binds Binds
334     *
335     * @return Builder
336     *
337     * @since 1.0.0
338     */
339    public function bind(string | array $binds) : self
340    {
341        if (\is_array($binds)) {
342            $this->binds += $binds;
343        } else {
344            $this->binds[] = $binds;
345        }
346
347        return $this;
348    }
349
350    /**
351     * {@inheritdoc}
352     */
353    public function toSql() : string
354    {
355        if (!empty($this->joins)) {
356            $this->resolveJoinDependencies();
357        }
358
359        $query = $this->grammar->compileQuery($this);
360
361        if (self::$log) {
362            \phpOMS\Log\FileLogger::getInstance()->debug($query);
363        }
364
365        return $query;
366    }
367
368    /**
369     * Resolves join dependencies
370     *
371     * @return void
372     *
373     * @since 1.0.0
374     */
375    private function resolveJoinDependencies() : void
376    {
377        // create dependencies
378        $dependencies = [];
379        foreach ($this->joins as $table => $_) {
380            $dependencies[$table] = [];
381
382            foreach ($this->ons[$table] as $on) {
383                if (!\is_string($on)) {
384                    continue;
385                }
386
387                if (\stripos($on['column'], '.')) {
388                    $dependencies[$table][] = \explode('.', $on['column'])[0];
389                }
390
391                if (\stripos($on['value'], '.')) {
392                    $dependencies[$table][] = \explode('.', $on['value'])[0];
393                }
394            }
395        }
396
397        // add from to existing dependencies
398        foreach ($this->from as $table => $from) {
399            $dependencies[$table] = [];
400        }
401
402        $resolved = DependencyResolver::resolve($dependencies);
403
404        // cyclomatic dependencies
405        if ($resolved === null) {
406            return;
407        }
408
409        $temp        = $this->joins;
410        $this->joins = [];
411        foreach ($resolved as $table) {
412            if (isset($temp[$table])) {
413                $this->joins[$table] = $temp[$table];
414            }
415        }
416    }
417
418    /**
419     * Set raw query.
420     *
421     * @param string $raw Raw query
422     *
423     * @return Builder
424     *
425     * @throws \Exception
426     *
427     * @since 1.0.0
428     */
429    public function raw(string $raw) : self
430    {
431        if (!$this->isValidReadOnly($raw)) {
432            throw new \Exception();
433        }
434
435        $this->type = QueryType::RAW;
436        $this->raw  = \rtrim($raw, ';');
437
438        return $this;
439    }
440
441    /**
442     * Tests if a string contains a non read only component in case the builder is read only.
443     * If the builder is not read only it will always return true
444     *
445     * @param string $raw Raw query
446     *
447     * @return bool
448     *
449     * @since 1.0.0
450     */
451    private function isValidReadOnly(string $raw) : bool
452    {
453        if (!$this->isReadOnly) {
454            return true;
455        }
456
457        $raw = \strtolower($raw);
458
459        if (\stripos($raw, 'insert') !== false
460            || \stripos($raw, 'update') !== false
461            || \stripos($raw, 'drop') !== false
462            || \stripos($raw, 'delete') !== false
463            || \stripos($raw, 'create') !== false
464            || \stripos($raw, 'alter') !== false
465        ) {
466            return false;
467        }
468
469        return true;
470    }
471
472    /**
473     * Is distinct.
474     *
475     * @return Builder
476     *
477     * @since 1.0.0
478     */
479    public function distinct() : self
480    {
481        $this->distinct = true;
482
483        return $this;
484    }
485
486    /**
487     * From.
488     *
489     * @param mixed ...$tables Tables
490     *
491     * @return Builder
492     *
493     * @throws \InvalidArgumentException
494     *
495     * @since 1.0.0
496     */
497    public function from(mixed ...$tables) : self
498    {
499        /** @var mixed[] $tables */
500        /** @var mixed $table */
501        foreach ($tables as $key => $table) {
502            if (\is_string($table) || $table instanceof self) {
503                $this->from[] = $table;
504            } else {
505                throw new \InvalidArgumentException();
506            }
507        }
508
509        return $this;
510    }
511
512    /**
513     * From with alias.
514     *
515     * @param mixed  $column Column query
516     * @param string $alias  Alias
517     *
518     * @return Builder
519     *
520     * @since 1.0.0
521     */
522    public function fromAs(mixed $column, string $alias) : self
523    {
524        $this->from[$alias] = $column;
525
526        return $this;
527    }
528
529    /**
530     * Where.
531     *
532     * @param string|array|Where $columns  Columns
533     * @param string|array       $operator Operator
534     * @param mixed              $values   Values
535     * @param string|array       $boolean  Boolean condition
536     *
537     * @return Builder
538     *
539     * @throws \InvalidArgumentException
540     *
541     * @since 1.0.0
542     */
543    public function where(string | array | self $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and') : self
544    {
545        if (!\is_array($columns)) {
546            $columns  = [$columns];
547            $operator = [$operator];
548            $values   = [$values];
549            $boolean  = [$boolean];
550        }
551
552        $i = 0;
553        foreach ($columns as $column) {
554            if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) {
555                throw new \InvalidArgumentException('Unknown operator: "' . $operator[$i] . '"');
556            }
557
558            $this->wheres[self::getPublicColumnName($column)][] = [
559                'column'   => $column,
560                'operator' => $operator[$i],
561                'value'    => $values[$i],
562                'boolean'  => $boolean[$i],
563            ];
564
565            ++$i;
566        }
567
568        return $this;
569    }
570
571    /**
572     * Where and sub condition.
573     *
574     * @param string|array|Where $where    Where sub condition
575     * @param string|array       $operator Operator
576     * @param mixed              $values   Values
577     *
578     * @return Builder
579     *
580     * @since 1.0.0
581     */
582    public function andWhere(string | array | Where $where, string | array $operator = null, mixed $values = null) : self
583    {
584        return $this->where($where, $operator, $values, 'and');
585    }
586
587    /**
588     * Where or sub condition.
589     *
590     * @param string|array|Where $where    Where sub condition
591     * @param string|array       $operator Operator
592     * @param mixed              $values   Values
593     *
594     * @return Builder
595     *
596     * @since 1.0.0
597     */
598    public function orWhere(string | array | self $where, string | array $operator = null, mixed $values = null) : self
599    {
600        return $this->where($where, $operator, $values, 'or');
601    }
602
603    /**
604     * Where in.
605     *
606     * @param string|array|Where $column  Column
607     * @param string|array       $values  Values
608     * @param string             $boolean Boolean condition
609     *
610     * @return Builder
611     *
612     * @since 1.0.0
613     */
614    public function whereIn(string | array | Where $column, mixed $values = null, string $boolean = 'and') : self
615    {
616        $this->where($column, 'in', $values, $boolean);
617
618        return $this;
619    }
620
621    /**
622     * Where null.
623     *
624     * @param string|array|Where $column  Column
625     * @param string             $boolean Boolean condition
626     *
627     * @return Builder
628     *
629     * @since 1.0.0
630     */
631    public function whereNull(string | array | Where $column, string $boolean = 'and') : self
632    {
633        $this->where($column, '=', null, $boolean);
634
635        return $this;
636    }
637
638    /**
639     * Where not null.
640     *
641     * @param string|array|Where $column  Column
642     * @param string             $boolean Boolean condition
643     *
644     * @return Builder
645     *
646     * @since 1.0.0
647     */
648    public function whereNotNull(string | array | Where $column, string $boolean = 'and') : self
649    {
650        $this->where($column, '!=', null, $boolean);
651
652        return $this;
653    }
654
655    /**
656     * Group by.
657     *
658     * @param mixed ...$columns Grouping result
659     *
660     * @return Builder
661     *
662     * @throws \InvalidArgumentException
663     *
664     * @since 1.0.0
665     */
666    public function groupBy(mixed ...$columns) : self
667    {
668        /** @var mixed[] $columns */
669        /** @var mixed $column */
670        foreach ($columns as $column) {
671            if (\is_string($column) || $column instanceof self) {
672                $this->groups[] = $column;
673            } else {
674                throw new \InvalidArgumentException();
675            }
676        }
677
678        return $this;
679    }
680
681    /**
682     * Order by newest.
683     *
684     * @param string $column Column
685     *
686     * @return Builder
687     *
688     * @since 1.0.0
689     */
690    public function newest(string $column) : self
691    {
692        $this->orderBy($column, OrderType::DESC);
693
694        return $this;
695    }
696
697    /**
698     * Order by oldest.
699     *
700     * @param string $column Column
701     *
702     * @return Builder
703     *
704     * @since 1.0.0
705     */
706    public function oldest(string $column) : self
707    {
708        $this->orderBy($column, OrderType::ASC);
709
710        return $this;
711    }
712
713    /**
714     * Order by.
715     *
716     * @param string|array    $columns Columns
717     * @param string|string[] $order   Orders
718     *
719     * @return Builder
720     *
721     * @since 1.0.0
722     */
723    public function orderBy(string | array $columns, string | array $order = OrderType::DESC) : self
724    {
725        if (\is_string($columns)) {
726            $columns = [$columns];
727        }
728
729        foreach ($columns as $key => $column) {
730            $tOrder = \is_string($order) ? $order : $order[$key];
731
732            $this->orders[$column] = $tOrder;
733        }
734
735        return $this;
736    }
737
738    /**
739     * Offset.
740     *
741     * @param int $offset Offset
742     *
743     * @return Builder
744     *
745     * @since 1.0.0
746     */
747    public function offset(int $offset) : self
748    {
749        $this->offset = $offset;
750
751        return $this;
752    }
753
754    /**
755     * Limit.
756     *
757     * @param int $limit Limit
758     *
759     * @return Builder
760     *
761     * @since 1.0.0
762     */
763    public function limit(int $limit) : self
764    {
765        $this->limit = $limit;
766
767        return $this;
768    }
769
770    /**
771     * Union.
772     *
773     * @param mixed $query Query
774     *
775     * @return Builder
776     *
777     * @since 1.0.0
778     */
779    public function union(mixed $query) : self
780    {
781        if (!\is_array($query)) {
782            $this->unions[] = $query;
783        } else {
784            $this->unions += $query;
785        }
786
787        return $this;
788    }
789
790    /**
791     * Lock query.
792     *
793     * @return void
794     *
795     * @since 1.0.0
796     */
797    public function lock() : void
798    {
799    }
800
801    /**
802     * Lock for update query.
803     *
804     * @return void
805     *
806     * @since 1.0.0
807     */
808    public function lockUpdate() : void
809    {
810    }
811
812    /**
813     * Create query string.
814     *
815     * @return string
816     *
817     * @since 1.0.0
818     */
819    public function __toString()
820    {
821        return $this->grammar->compileQuery($this);
822    }
823
824    /**
825     * Find query.
826     *
827     * @return void
828     *
829     * @since 1.0.0
830     */
831    public function find() : void
832    {
833    }
834
835    /**
836     * Count results.
837     *
838     * @param string $table Table to count the result set
839     *
840     * @return Builder
841     *
842     * @since 1.0.0
843     */
844    public function count(string $table = '*') : self
845    {
846        /**
847         * @todo
848         *  Don't do this as a string, create a new object $this->select(new Count($table)).
849         *  The parser should be able to handle this much better
850         */
851        return $this->select('COUNT(' . $table . ')');
852    }
853
854    /**
855     * Select minimum.
856     *
857     * @return void
858     *
859     * @since 1.0.0
860     */
861    public function min() : void
862    {
863    }
864
865    /**
866     * Select maximum.
867     *
868     * @return void
869     *
870     * @since 1.0.0
871     */
872    public function max() : void
873    {
874    }
875
876    /**
877     * Select sum.
878     *
879     * @return void
880     *
881     * @since 1.0.0
882     */
883    public function sum() : void
884    {
885    }
886
887    /**
888     * Select average.
889     *
890     * @return void
891     *
892     * @since 1.0.0
893     */
894    public function avg() : void
895    {
896    }
897
898    /**
899     * Insert into columns.
900     *
901     * @param mixed ...$columns Columns
902     *
903     * @return Builder
904     *
905     * @throws \Exception
906     *
907     * @since 1.0.0
908     */
909    public function insert(mixed ...$columns) : self
910    {
911        if ($this->isReadOnly) {
912            throw new \Exception();
913        }
914
915        $this->type = QueryType::INSERT;
916
917        foreach ($columns as $column) {
918            $this->inserts[] = $column;
919        }
920
921        return $this;
922    }
923
924    /**
925     * Table to insert into.
926     *
927     * @param string $table Table
928     *
929     * @return Builder
930     *
931     * @since 1.0.0
932     */
933    public function into(string $table) : self
934    {
935        $this->into = $table;
936
937        return $this;
938    }
939
940    /**
941     * Values to insert.
942     *
943     * @param mixed ...$values Values
944     *
945     * @return Builder
946     *
947     * @since 1.0.0
948     */
949    public function values(mixed ...$values) : self
950    {
951        $this->values[] = $values;
952
953        return $this;
954    }
955
956    /**
957     * Get insert values
958     *
959     * @return array
960     *
961     * @since 1.0.0
962     */
963    public function getValues() : array
964    {
965        return $this->values;
966    }
967
968    /**
969     * Values to insert.
970     *
971     * @param mixed $value Values
972     *
973     * @return Builder
974     *
975     * @since 1.0.0
976     */
977    public function value(mixed $value) : self
978    {
979        \end($this->values);
980
981        $key   = \key($this->values);
982        $key ??= 0;
983
984        if (\is_array($value)) {
985            $this->values[$key + 1] = $value;
986        } else {
987            $this->values[$key][] = $value;
988        }
989
990        \reset($this->values);
991
992        return $this;
993    }
994
995    /**
996     * Values to insert.
997     *
998     * @param mixed ...$sets Values
999     *
1000     * @return Builder
1001     *
1002     * @since 1.0.0
1003     */
1004    public function sets(mixed ...$sets) : self
1005    {
1006        $this->sets[$sets[0]] = $sets[1] ?? null;
1007
1008        return $this;
1009    }
1010
1011    /**
1012     * Values to insert.
1013     *
1014     * @param mixed $set Values
1015     *
1016     * @return Builder
1017     *
1018     * @since 1.0.0
1019     */
1020    public function set(mixed $set) : self
1021    {
1022        $this->sets[\key($set)] = \current($set);
1023
1024        return $this;
1025    }
1026
1027    /**
1028     * Update columns.
1029     *
1030     * @param string $table Column names to update
1031     *
1032     * @return Builder
1033     *
1034     * @throws \Exception
1035     * @throws \InvalidArgumentException
1036     *
1037     * @since 1.0.0
1038     */
1039    public function update(string $table) : self
1040    {
1041        if ($this->isReadOnly) {
1042            throw new \Exception();
1043        }
1044
1045        $this->type      = QueryType::UPDATE;
1046        $this->updates[] = $table;
1047
1048        return $this;
1049    }
1050
1051    /**
1052     * Delete query
1053     *
1054     * @return Builder
1055     *
1056     * @since 1.0.0
1057     */
1058    public function delete() : self
1059    {
1060        if ($this->isReadOnly) {
1061            throw new \Exception();
1062        }
1063
1064        $this->type = QueryType::DELETE;
1065
1066        return $this;
1067    }
1068
1069    /**
1070     * Increment value.
1071     *
1072     * @return void
1073     *
1074     * @since 1.0.0
1075     */
1076    public function increment() : void
1077    {
1078    }
1079
1080    /**
1081     * Decrement value.
1082     *
1083     * @return void
1084     *
1085     * @since 1.0.0
1086     */
1087    public function decrement() : void
1088    {
1089    }
1090
1091    /**
1092     * Join.
1093     *
1094     * @param string|self $table Join query
1095     * @param string      $type  Join type
1096     * @param null|string $alias Alias name (empty = none)
1097     *
1098     * @return Builder
1099     *
1100     * @since 1.0.0
1101     */
1102    public function join(string | self $table, string $type = JoinType::JOIN, string $alias = null) : self
1103    {
1104        $this->joins[$alias ?? $table] = ['type' => $type, 'table' => $table, 'alias' => $alias];
1105
1106        return $this;
1107    }
1108
1109    /**
1110     * Join.
1111     *
1112     * @param string|self $table Join query
1113     * @param null|string $alias Alias name (empty = none)
1114     *
1115     * @return Builder
1116     *
1117     * @since 1.0.0
1118     */
1119    public function leftJoin(string | self $table, string $alias = null) : self
1120    {
1121        return $this->join($table, JoinType::LEFT_JOIN, $alias);
1122    }
1123
1124    /**
1125     * Join.
1126     *
1127     * @param string|self $table Join query
1128     * @param null|string $alias Alias name (empty = none)
1129     *
1130     * @return Builder
1131     *
1132     * @since 1.0.0
1133     */
1134    public function leftOuterJoin(string | self $table, string $alias = null) : self
1135    {
1136        return $this->join($table, JoinType::LEFT_OUTER_JOIN, $alias);
1137    }
1138
1139    /**
1140     * Join.
1141     *
1142     * @param string|self $table Join query
1143     * @param null|string $alias Alias name (empty = none)
1144     *
1145     * @return Builder
1146     *
1147     * @since 1.0.0
1148     */
1149    public function leftInnerJoin(string | self $table, string $alias = null) : self
1150    {
1151        return $this->join($table, JoinType::LEFT_INNER_JOIN, $alias);
1152    }
1153
1154    /**
1155     * Join.
1156     *
1157     * @param string|self $table Join query
1158     * @param null|string $alias Alias name (empty = none)
1159     *
1160     * @return Builder
1161     *
1162     * @since 1.0.0
1163     */
1164    public function rightJoin(string | self $table, string $alias = null) : self
1165    {
1166        return $this->join($table, JoinType::RIGHT_JOIN, $alias);
1167    }
1168
1169    /**
1170     * Join.
1171     *
1172     * @param string|self $table Join query
1173     * @param null|string $alias Alias name (empty = none)
1174     *
1175     * @return Builder
1176     *
1177     * @since 1.0.0
1178     */
1179    public function rightOuterJoin(string | self $table, string $alias = null) : self
1180    {
1181        return $this->join($table, JoinType::RIGHT_OUTER_JOIN, $alias);
1182    }
1183
1184    /**
1185     * Join.
1186     *
1187     * @param string|self $table Join query
1188     * @param null|string $alias Alias name (empty = none)
1189     *
1190     * @return Builder
1191     *
1192     * @since 1.0.0
1193     */
1194    public function rightInnerJoin(string | self $table, string $alias = null) : self
1195    {
1196        return $this->join($table, JoinType::RIGHT_INNER_JOIN, $alias);
1197    }
1198
1199    /**
1200     * Join.
1201     *
1202     * @param string|self $table Join query
1203     * @param null|string $alias Alias name (empty = none)
1204     *
1205     * @return Builder
1206     *
1207     * @since 1.0.0
1208     */
1209    public function outerJoin(string | self $table, string $alias = null) : self
1210    {
1211        return $this->join($table, JoinType::OUTER_JOIN, $alias);
1212    }
1213
1214    /**
1215     * Join.
1216     *
1217     * @param string|self $table Join query
1218     * @param null|string $alias Alias name (empty = none)
1219     *
1220     * @return Builder
1221     *
1222     * @since 1.0.0
1223     */
1224    public function innerJoin(string | self $table, string $alias = null) : self
1225    {
1226        return $this->join($table, JoinType::INNER_JOIN, $alias);
1227    }
1228
1229    /**
1230     * Join.
1231     *
1232     * @param string|self $table Join query
1233     * @param null|string $alias Alias name (empty = none)
1234     *
1235     * @return Builder
1236     *
1237     * @since 1.0.0
1238     */
1239    public function crossJoin(string | self $table, string $alias = null) : self
1240    {
1241        return $this->join($table, JoinType::CROSS_JOIN, $alias);
1242    }
1243
1244    /**
1245     * Join.
1246     *
1247     * @param string|self $table Join query
1248     * @param null|string $alias Alias name (empty = none)
1249     *
1250     * @return Builder
1251     *
1252     * @since 1.0.0
1253     */
1254    public function fullJoin(string | self $table, string $alias = null) : self
1255    {
1256        return $this->join($table, JoinType::FULL_JOIN, $alias);
1257    }
1258
1259    /**
1260     * Join.
1261     *
1262     * @param string|self $table Join query
1263     * @param null|string $alias Alias name (empty = none)
1264     *
1265     * @return Builder
1266     *
1267     * @since 1.0.0
1268     */
1269    public function fullOuterJoin(string | self $table, string $alias = null) : self
1270    {
1271        return $this->join($table, JoinType::FULL_OUTER_JOIN, $alias);
1272    }
1273
1274    /**
1275     * Rollback.
1276     *
1277     * @return Builder
1278     *
1279     * @since 1.0.0
1280     */
1281    public function rollback() : self
1282    {
1283        return $this;
1284    }
1285
1286    /**
1287     * On.
1288     *
1289     * @param string|array      $columns  Columns to join on
1290     * @param null|string|array $operator Comparison operator
1291     * @param null|string|array $values   Values to compare with
1292     * @param string|array      $boolean  Concatonator
1293     * @param null|string       $table    Table this belongs to
1294     *
1295     * @return Builder
1296     *
1297     * @throws \InvalidArgumentException
1298     *
1299     * @since 1.0.0
1300     */
1301    public function on(string | array $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and', string $table = null) : self
1302    {
1303        if (!\is_array($columns)) {
1304            $columns  = [$columns];
1305            $operator = [$operator];
1306            $values   = [$values];
1307            $boolean  = [$boolean];
1308        }
1309
1310        $joinCount = \count($this->joins) - 1;
1311        $i         = 0;
1312        $table   ??= \array_keys($this->joins)[$joinCount];
1313
1314        foreach ($columns as $column) {
1315            if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) {
1316                throw new \InvalidArgumentException('Unknown operator.');
1317            }
1318
1319            $this->ons[$table][] = [
1320                'column'   => $column,
1321                'operator' => $operator[$i],
1322                'value'    => $values[$i],
1323                'boolean'  => $boolean[$i],
1324            ];
1325
1326            ++$i;
1327        }
1328
1329        return $this;
1330    }
1331
1332    /**
1333     * On.
1334     *
1335     * @param string|array      $columns  Columns to join on
1336     * @param null|string|array $operator Comparison operator
1337     * @param null|string|array $values   Values to compare with
1338     *
1339     * @return Builder
1340     *
1341     * @since 1.0.0
1342     */
1343    public function orOn(string | array $columns, string | array $operator = null, string | array $values = null) : self
1344    {
1345        return $this->on($columns, $operator, $values, 'or');
1346    }
1347
1348    /**
1349     * On.
1350     *
1351     * @param string|array      $columns  Columns to join on
1352     * @param null|string|array $operator Comparison operator
1353     * @param null|string|array $values   Values to compare with
1354     *
1355     * @return Builder
1356     *
1357     * @since 1.0.0
1358     */
1359    public function andOn(string | array $columns, string | array $operator = null, string | array $values = null) : self
1360    {
1361        return $this->on($columns, $operator, $values, 'and');
1362    }
1363
1364    /**
1365     * Merging query.
1366     *
1367     * Merging query in order to remove database query volume
1368     *
1369     * @return Builder
1370     *
1371     * @since 1.0.0
1372     */
1373    public function merge() : self
1374    {
1375        return clone($this);
1376    }
1377
1378    /**
1379     * {@inheritdoc}
1380     */
1381    public function execute() : ?\PDOStatement
1382    {
1383        $sth = null;
1384
1385        try {
1386            $sth = $this->connection->con->prepare($this->toSql());
1387            if ($sth === false) {
1388                return null;
1389            }
1390
1391            foreach ($this->binds as $key => $bind) {
1392                $type = self::getBindParamType($bind);
1393
1394                $sth->bindParam($key, $bind, $type);
1395            }
1396
1397            $sth->execute();
1398        } catch (\Throwable $t) {
1399            // @codeCoverageIgnoreStart
1400            \phpOMS\Log\FileLogger::getInstance()->error(
1401                \phpOMS\Log\FileLogger::MSG_FULL, [
1402                    'message' => $t->getMessage() . ':' . $this->toSql(),
1403                    'line'    => __LINE__,
1404                    'file'    => self::class,
1405                ]
1406            );
1407
1408            $sth = null;
1409            // @codeCoverageIgnoreEnd
1410        }
1411
1412        return $sth;
1413    }
1414
1415    /**
1416     * Get bind parameter type.
1417     *
1418     * @param mixed $value Value to bind
1419     *
1420     * @return int
1421     *
1422     * @throws \Exception
1423     *
1424     * @since 1.0.0
1425     */
1426    public static function getBindParamType(mixed $value) : int
1427    {
1428        if (\is_int($value)) {
1429            return \PDO::PARAM_INT;
1430        } elseif (\is_string($value) || \is_float($value)) {
1431            return \PDO::PARAM_STR;
1432        }
1433
1434        throw new \Exception();
1435    }
1436
1437    /**
1438     * Get column name
1439     *
1440     * @param mixed $column Column name
1441     *
1442     * @return string
1443     *
1444     * @throws \Exception
1445     *
1446     * @since 1.0.0
1447     */
1448    public static function getPublicColumnName(mixed $column) : string
1449    {
1450        if (\is_string($column)) {
1451            return $column;
1452        } elseif ($column instanceof Column) {
1453            return $column->getColumn();
1454        } elseif ($column instanceof SerializableInterface) {
1455            return $column->serialize();
1456        } elseif ($column instanceof self) {
1457            return \md5($column->toSql());
1458        }
1459
1460        throw new \Exception();
1461    }
1462}