Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.57% covered (warning)
80.57%
141 / 175
26.09% covered (danger)
26.09%
6 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Grammar
80.57% covered (warning)
80.57%
141 / 175
26.09% covered (danger)
26.09%
6 / 23
163.59
0.00% covered (danger)
0.00%
0 / 1
 compileComponents
91.38% covered (success)
91.38%
53 / 58
0.00% covered (danger)
0.00%
0 / 1
28.50
 compileSelects
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 compileRandom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileUpdates
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 compileDeletes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileFrom
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 compileWheres
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 compileWhereElement
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
13.78
 compileWhereQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileFromQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileJoins
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
11.08
 compileOn
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 compileOnElement
50.00% covered (danger)
50.00%
9 / 18
0.00% covered (danger)
0.00%
0 / 1
22.50
 compileGroups
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 compileOrders
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 compileUnions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileLock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileInto
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compileInserts
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 compileValues
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 compileSets
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\DataStorage\Database\Query\Grammar
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\Grammar;
16
17use phpOMS\DataStorage\Database\BuilderAbstract;
18use phpOMS\DataStorage\Database\GrammarAbstract;
19use phpOMS\DataStorage\Database\Query\Builder;
20use phpOMS\DataStorage\Database\Query\From;
21use phpOMS\DataStorage\Database\Query\QueryType;
22use phpOMS\DataStorage\Database\Query\Where;
23
24/**
25 * Database query grammar.
26 *
27 * @package phpOMS\DataStorage\Database\Query\Grammar
28 * @license OMS License 2.0
29 * @link    https://jingga.app
30 * @since   1.0.0
31 *
32 * @todo Karaka/phpOMS#33
33 *  Implement missing grammar & builder functions
34 *  Missing elements are e.g. sum, merge etc.
35 */
36class Grammar extends GrammarAbstract
37{
38    /**
39     * {@inheritdoc}
40     *
41     * @throws \InvalidArgumentException
42     */
43    protected function compileComponents(BuilderAbstract $query) : array
44    {
45        /** @var Builder $query */
46
47        $sql = [];
48        switch ($query->getType()) {
49            case QueryType::SELECT:
50                // $sql[] = $this->compileAggregate($query, $query->aggregate);
51                if (!empty($query->selects)) {
52                    $sql[] = $this->compileSelects($query, $query->selects);
53                }
54
55                if (!empty($query->from)) {
56                    $sql[] = $this->compileFrom($query, $query->from);
57                }
58
59                if (!empty($query->joins)) {
60                    $sql[] = $this->compileJoins($query, $query->joins);
61                }
62
63                if (!empty($query->wheres)) {
64                    $sql[] = $this->compileWheres($query, $query->wheres);
65                }
66
67                // $sql[] = $this->compileHavings($query, $query->havings);
68
69                if (!empty($query->groups)) {
70                    $sql[] = $this->compileGroups($query, $query->groups);
71                }
72
73                if (!empty($query->orders)) {
74                    $sql[] = $this->compileOrders($query, $query->orders);
75                }
76
77                if (!empty($query->limit)) {
78                    $sql[] = $this->compileLimit($query, $query->limit);
79                }
80
81                if (!empty($query->offset)) {
82                    $sql[] = $this->compileOffset($query, $query->offset);
83                }
84
85                if (!empty($query->unions)) {
86                    $sql[] = $this->compileUnions();
87                }
88
89                if (!empty($query->lock)) {
90                    $sql[] = $this->compileLock();
91                }
92
93                break;
94            case QueryType::INSERT:
95                if (!empty($query->into)) {
96                    $sql[] = $this->compileInto($query, $query->into);
97                }
98
99                if (!empty($query->inserts)) {
100                    $sql[] = $this->compileInserts($query, $query->inserts);
101                }
102
103                if (!empty($query->values)) {
104                    $sql[] = $this->compileValues($query, $query->values);
105                }
106
107                break;
108            case QueryType::UPDATE:
109                if (!empty($query->updates)) {
110                    $sql[] = $this->compileUpdates($query, $query->updates);
111                }
112
113                if (!empty($query->sets)) {
114                    $sql[] = $this->compileSets($query, $query->sets);
115                }
116
117                if (!empty($query->wheres)) {
118                    $sql[] = $this->compileWheres($query, $query->wheres);
119                }
120
121                break;
122            case QueryType::DELETE:
123                if (!empty($query->deletes)) {
124                    $sql[] = $this->compileDeletes($query, $query->deletes);
125                }
126
127                if (!empty($query->from)) {
128                    $sql[] = $this->compileFrom($query, $query->from);
129                }
130
131                if (!empty($query->wheres)) {
132                    $sql[] = $this->compileWheres($query, $query->wheres);
133                }
134
135                break;
136            case QueryType::RANDOM:
137                $sql[] = $this->compileRandom($query, $query->random);
138
139                break;
140            case QueryType::RAW:
141                $sql[] = $query->raw;
142
143                break;
144            case QueryType::NONE:
145                return [];
146            default:
147                throw new \InvalidArgumentException('Unknown query type.');
148        }
149
150        return $sql;
151    }
152
153    /**
154     * Compile select.
155     *
156     * @param Builder $query   Builder
157     * @param array   $columns Columns
158     *
159     * @return string
160     *
161     * @since 1.0.0
162     */
163    protected function compileSelects(Builder $query, array $columns) : string
164    {
165        $expression = $this->expressionizeTableColumn($columns, false);
166
167        if ($expression === '') {
168            $expression = '*';
169        }
170
171        return ($query->distinct ? 'SELECT DISTINCT ' : 'SELECT ') . $expression;
172    }
173
174    /**
175     * Get random entry
176     *
177     * @param Builder $query   Builder
178     * @param array   $columns Columns
179     *
180     * @return string
181     *
182     * @since 1.0.0
183     */
184    protected function compileRandom(Builder $query, array $columns) : string
185    {
186        return '';
187    }
188
189    /**
190     * Compile select.
191     *
192     * @param Builder $query Builder
193     * @param array   $table Table
194     *
195     * @return string
196     *
197     * @since 1.0.0
198     */
199    protected function compileUpdates(Builder $query, array $table) : string
200    {
201        $expression = $this->expressionizeTableColumn($table);
202
203        if ($expression === '') {
204            return '';
205        }
206
207        return 'UPDATE ' . $expression;
208    }
209
210    /**
211     * Compile select.
212     *
213     * @param Builder $query   Builder
214     * @param array   $columns Columns
215     *
216     * @return string
217     *
218     * @since 1.0.0
219     */
220    protected function compileDeletes(Builder $query, array $columns) : string
221    {
222        return 'DELETE';
223    }
224
225    /**
226     * Compile from.
227     *
228     * @param Builder $query Builder
229     * @param array   $table Tables
230     *
231     * @return string
232     *
233     * @since 1.0.0
234     */
235    protected function compileFrom(Builder $query, array $table) : string
236    {
237        $expression = $this->expressionizeTableColumn($table);
238
239        if ($expression === '') {
240            return '';
241        }
242
243        return 'FROM ' . $expression;
244    }
245
246    /**
247     * Compile where.
248     *
249     * @param Builder $query  Builder
250     * @param array   $wheres Where elmenets
251     * @param bool    $first  Is first element (usefull for nesting)
252     *
253     * @return string
254     *
255     * @since 1.0.0
256     */
257    protected function compileWheres(Builder $query, array $wheres, bool $first = true) : string
258    {
259        $expression = '';
260
261        foreach ($wheres as $where) {
262            foreach ($where as $element) {
263                $expression .= $this->compileWhereElement($element, $query, $first);
264                $first       = false;
265            }
266        }
267
268        if ($expression === '') {
269            return '';
270        }
271
272        return 'WHERE ' . $expression;
273    }
274
275    /**
276     * Compile where element.
277     *
278     * @param array   $element Element data
279     * @param Builder $query   Query builder
280     * @param bool    $first   Is first element (usefull for nesting)
281     *
282     * @return string
283     *
284     * @since 1.0.0
285     */
286    protected function compileWhereElement(array $element, Builder $query, bool $first = true) : string
287    {
288        $expression = '';
289
290        if (!$first) {
291            $expression = ' ' . \strtoupper($element['boolean']) . ' ';
292        }
293
294        if (\is_string($element['column'])) {
295            $expression .= $this->compileSystem($element['column']);
296        } elseif ($element['column'] instanceof \Closure) {
297            $expression .= $element['column']();
298        } elseif ($element['column'] instanceof Where) {
299            $where       = \rtrim($this->compileWhereQuery($element['column']), ';');
300            $expression .= '(' . (\str_starts_with($where, 'WHERE ') ? \substr($where, 6) : $where) . ')';
301        } elseif ($element['column'] instanceof Builder) {
302            $expression .= '(' . \rtrim($element['column']->toSql(), ';') . ')';
303        }
304
305        if (isset($element['value']) && (!empty($element['value']) || !\is_array($element['value']))) {
306            $expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileValue($query, $element['value']);
307        } elseif ($element['value'] === null && !($element['column'] instanceof Builder)) {
308            $operator    = $element['operator'] === '=' ? 'IS' : 'IS NOT';
309            $expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']);
310        }
311
312        return $expression;
313    }
314
315    /**
316     * Compile where query.
317     *
318     * @param Where $where Where query
319     *
320     * @return string
321     *
322     * @since 1.0.0
323     */
324    protected function compileWhereQuery(Where $where) : string
325    {
326        return $where->toSql();
327    }
328
329    /**
330     * Compile from query.
331     *
332     * @param From $from Where query
333     *
334     * @return string
335     *
336     * @since 1.0.0
337     */
338    protected function compileFromQuery(From $from) : string
339    {
340        return $from->toSql();
341    }
342
343    /**
344     * Compile limit.
345     *
346     * @param Builder $query Builder
347     * @param int     $limit Limit
348     *
349     * @return string
350     *
351     * @since 1.0.0
352     */
353    protected function compileLimit(Builder $query, int $limit) : string
354    {
355        return 'LIMIT ' . $limit;
356    }
357
358    /**
359     * Compile offset.
360     *
361     * @param Builder $query  Builder
362     * @param int     $offset Offset
363     *
364     * @return string
365     *
366     * @since 1.0.0
367     */
368    protected function compileOffset(Builder $query, int $offset) : string
369    {
370        return 'OFFSET ' . $offset;
371    }
372
373    /**
374     * Compile joins.
375     *
376     * @param Builder $query Builder
377     * @param array   $joins Joins
378     *
379     * @return string
380     *
381     * @since 1.0.0
382     */
383    protected function compileJoins(Builder $query, array $joins) : string
384    {
385        $expression = '';
386
387        foreach ($joins as $table => $join) {
388            $expression .= $join['type'] . ' ';
389
390            if (\is_string($join['table'])) {
391                $expression .= $this->compileSystem($join['table']) . (\is_string($join['alias']) ? ' as ' . $join['alias'] : '');
392            } elseif ($join['table'] instanceof \Closure) {
393                $expression .= $join['table']() . (\is_string($join['alias']) ? ' as ' . $join['alias'] : '');
394            } elseif ($join['table'] instanceof Builder) {
395                $expression .= '(' . \rtrim($join['table']->toSql(), ';') . ')' . (\is_string($join['alias']) ? ' as ' . $join['alias'] : '');
396            }
397
398            $expression .= $this->compileOn($query, $query->ons[$join['alias'] ?? $table]) . ' ';
399        }
400
401        return \rtrim($expression, ', ');
402    }
403
404    /**
405     * Compile on.
406     *
407     * @param Builder $query Builder
408     * @param array   $ons   On values
409     * @param bool    $first Is first on element
410     *
411     * @return string
412     *
413     * @since 1.0.0
414     */
415    protected function compileOn(Builder $query, array $ons, bool $first = true) : string
416    {
417        $expression = '';
418
419        foreach ($ons as $on) {
420            $expression .= $this->compileOnElement($on, $query, $first);
421            $first       = false;
422        }
423
424        if ($expression === '') {
425            return '';
426        }
427
428        return ' ON ' . $expression;
429    }
430
431    /**
432     * Compile where element.
433     *
434     * @param array   $element Element data
435     * @param Builder $query   Query builder
436     * @param bool    $first   Is first element (usefull for nesting)
437     *
438     * @return string
439     *
440     * @since 1.0.0
441     */
442    protected function compileOnElement(array $element, Builder $query, bool $first = true) : string
443    {
444        $expression = '';
445
446        if (!$first) {
447            $expression = ' ' . \strtoupper($element['boolean']) . ' ';
448        }
449
450        if (\is_string($element['column'])) {
451            // handle bug when no table is specified in the where column
452            if (\count($query->from) === 1 && \stripos($element['column'], '.') === false) {
453                $element['column'] = $query->from[0] . '.' . $element['column'];
454            }
455
456            $expression .= $this->compileSystem($element['column']);
457        } elseif ($element['column'] instanceof \Closure) {
458            $expression .= $element['column']();
459        } elseif ($element['column'] instanceof Builder) {
460            $expression .= '(' . $element['column']->toSql() . ')';
461        } elseif ($element['column'] instanceof Where) {
462            $expression .= '(' . \rtrim($this->compileWhereQuery($element['column']), ';') . ')';
463        }
464
465        // @todo: on doesn't allow values as value (only table column names). This is bad and needs to be fixed!
466        if (isset($element['value'])) {
467            $expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileSystem($element['value']);
468        } else {
469            $operator    = $element['operator'] === '=' ? 'IS' : 'IS NOT';
470            $expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']);
471        }
472
473        return $expression;
474    }
475
476    /**
477     * Compile offset.
478     *
479     * @param Builder $query  Builder
480     * @param array   $groups Groups
481     *
482     * @return string
483     *
484     * @since 1.0.0
485     */
486    protected function compileGroups(Builder $query, array $groups) : string
487    {
488        $expression = '';
489
490        foreach ($groups as $group) {
491            $expression .= $this->compileSystem($group) . ', ';
492        }
493
494        $expression = \rtrim($expression, ', ');
495
496        return 'GROUP BY ' . $expression;
497    }
498
499    /**
500     * Compile offset.
501     *
502     * @param Builder $query  Builder
503     * @param array   $orders Order
504     *
505     * @return string
506     *
507     * @since 1.0.0
508     */
509    protected function compileOrders(Builder $query, array $orders) : string
510    {
511        $expression = '';
512
513        foreach ($orders as $column => $order) {
514            $expression .= $this->compileSystem($column) . ' ' . $order . ', ';
515        }
516
517        $expression = \rtrim($expression, ', ');
518
519        if ($expression === '') {
520            return '';
521        }
522
523        return 'ORDER BY ' . $expression;
524    }
525
526    /**
527     * Compile unions.
528     *
529     * @return string
530     *
531     * @since 1.0.0
532     */
533    protected function compileUnions() : string
534    {
535        return '';
536    }
537
538    /**
539     * Compile lock.
540     *
541     * @return string
542     *
543     * @since 1.0.0
544     */
545    protected function compileLock() : string
546    {
547        return '';
548    }
549
550    /**
551     * Compile insert into table.
552     *
553     * @param Builder $query Builder
554     * @param string  $table Table
555     *
556     * @return string
557     *
558     * @since 1.0.0
559     */
560    protected function compileInto(Builder $query, string $table) : string
561    {
562        return 'INSERT INTO ' . $this->compileSystem($table);
563    }
564
565    /**
566     * Compile insert into columns.
567     *
568     * @param Builder $query   Builder
569     * @param array   $columns Columns
570     *
571     * @return string
572     *
573     * @since 1.0.0
574     */
575    protected function compileInserts(Builder $query, array $columns) : string
576    {
577        $count = \count($columns) - 1;
578
579        if ($count === -1) {
580            return '';
581        }
582
583        $cols = '(';
584        for ($i = 0; $i < $count; ++$i) {
585            $cols .= $this->compileSystem($columns[$i]) . ', ';
586        }
587
588        return $cols .= $this->compileSystem($columns[$count]) . ')';
589    }
590
591    /**
592     * Compile insert values.
593     *
594     * @param Builder $query  Builder
595     * @param array   $values Values
596     *
597     * @return string
598     *
599     * @since 1.0.0
600     */
601    protected function compileValues(Builder $query, array $values) : string
602    {
603        $values = \array_values($values);
604        $count  = \count($values) - 1;
605        if ($count === -1) {
606            return '';
607        }
608
609        $vals = 'VALUES ';
610        for ($i = 0; $i < $count; ++$i) {
611            $vals .= $this->compileValue($query, $values[$i]) . ', ';
612        }
613
614        return $vals . $this->compileValue($query, $values[$count]);
615    }
616
617    /**
618     * Compile insert values.
619     *
620     * @param Builder $query  Builder
621     * @param array   $values Values
622     *
623     * @return string
624     *
625     * @since 1.0.0
626     */
627    protected function compileSets(Builder $query, array $values) : string
628    {
629        $vals = '';
630
631        foreach ($values as $column => $value) {
632            $expression = $this->expressionizeTableColumn([$column], false);
633
634            $vals .= $expression . ' = ' . $this->compileValue($query, $value) . ', ';
635        }
636
637        if ($vals === '') {
638            return '';
639        }
640
641        return 'SET ' . \rtrim($vals, ', ');
642    }
643}