Passed
Push — 2.x ( 603a00...273a1a )
by Aleksei
04:35
created

Compiler::deleteQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 3
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver;
13
14
use Cycle\Database\Exception\CompilerException;
15
use Cycle\Database\Injection\FragmentInterface;
16
use Cycle\Database\Injection\Parameter;
17
use Cycle\Database\Injection\ParameterInterface;
18
use Cycle\Database\Query\QueryParameters;
19
20
abstract class Compiler implements CompilerInterface
21
{
22
    private Quoter $quoter;
23
24
    /**
25
     * @psalm-param non-empty-string $quotes
26
     */
27 82
    public function __construct(string $quotes = '""')
28
    {
29 82
        $this->quoter = new Quoter('', $quotes);
30 82
    }
31
32
    /**
33
     * @psalm-param non-empty-string $identifier
34
     *
35
     * @psalm-return non-empty-string
36
     */
37 3460
    public function quoteIdentifier(string $identifier): string
38
    {
39 3460
        return $this->quoter->identifier($identifier);
40
    }
41
42
    /**
43
     * @psalm-return non-empty-string
44
     */
45 2254
    public function compile(
46
        QueryParameters $params,
47
        string $prefix,
48
        FragmentInterface $fragment
49
    ): string {
50 2254
        return $this->fragment(
51
            $params,
52 2254
            $this->quoter->withPrefix($prefix),
53
            $fragment,
54 2254
            false
55
        );
56
    }
57
58
    /**
59
     * @psalm-return non-empty-string
60
     */
61 1344
    public function hashLimit(QueryParameters $params, array $tokens): string
62
    {
63 1344
        if ($tokens['limit'] !== null) {
64 66
            $params->push(new Parameter($tokens['limit']));
65
        }
66
67 1344
        if ($tokens['offset'] !== null) {
68 48
            $params->push(new Parameter($tokens['offset']));
69
        }
70
71 1344
        return '_' . ($tokens['limit'] === null) . '_' . ($tokens['offset'] === null);
72
    }
73
74
    /**
75
     * @psalm-return non-empty-string
76
     */
77 2254
    protected function fragment(
78
        QueryParameters $params,
79
        Quoter $q,
80
        FragmentInterface $fragment,
81
        bool $nestedQuery = true
82
    ): string {
83 2254
        $tokens = $fragment->getTokens();
84
85 2254
        switch ($fragment->getType()) {
86 2254
            case self::FRAGMENT:
87 662
                foreach ($tokens['parameters'] as $param) {
88 16
                    $params->push($param);
89
                }
90
91 662
                return $tokens['fragment'];
92
93 1674
            case self::EXPRESSION:
94 342
                foreach ($tokens['parameters'] as $param) {
95 26
                    $params->push($param);
96
                }
97
98 342
                return $q->quote($tokens['expression']);
99
100 1670
            case self::INSERT_QUERY:
101 272
                return $this->insertQuery($params, $q, $tokens);
102
103 1560
            case self::SELECT_QUERY:
104 1440
                if ($nestedQuery) {
105 112
                    if ($fragment->getPrefix() !== null) {
0 ignored issues
show
Bug introduced by
The method getPrefix() does not exist on Cycle\Database\Injection\FragmentInterface. It seems like you code against a sub-type of Cycle\Database\Injection\FragmentInterface such as Cycle\Database\Query\QueryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
                    if ($fragment->/** @scrutinizer ignore-call */ getPrefix() !== null) {
Loading history...
106 72
                        $q = $q->withPrefix(
107 72
                            $fragment->getPrefix(),
108 72
                            true
109
                        );
110
                    }
111
112 112
                    return sprintf(
113 112
                        '(%s)',
114 112
                        $this->selectQuery($params, $q, $tokens)
115
                    );
116
                }
117
118 1432
                return $this->selectQuery($params, $q, $tokens);
119
120 160
            case self::UPDATE_QUERY:
121 104
                return $this->updateQuery($params, $q, $tokens);
122
123 56
            case self::DELETE_QUERY:
124 56
                return $this->deleteQuery($params, $q, $tokens);
125
        }
126
127
        throw new CompilerException(
128
            sprintf(
129
                'Unknown fragment type %s',
130
                $fragment->getType()
131
            )
132
        );
133
    }
134
135
    /**
136
     * @psalm-return non-empty-string
137
     */
138 236
    protected function insertQuery(QueryParameters $params, Quoter $q, array $tokens): string
139
    {
140 236
        $values = [];
141 236
        foreach ($tokens['values'] as $value) {
142 228
            $values[] = $this->value($params, $q, $value);
143
        }
144
145 236
        if ($tokens['columns'] === []) {
146 8
            return sprintf(
147 8
                'INSERT INTO %s DEFAULT VALUES',
148 8
                $this->name($params, $q, $tokens['table'], true)
149
            );
150
        }
151
152 228
        return sprintf(
153 228
            'INSERT INTO %s (%s) VALUES %s',
154 228
            $this->name($params, $q, $tokens['table'], true),
155 228
            $this->columns($params, $q, $tokens['columns']),
156 228
            implode(', ', $values)
157
        );
158
    }
159
160
    /**
161
     * @psalm-return non-empty-string
162
     */
163 990
    protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string
164
    {
165
        // This statement(s) parts should be processed first to define set of table and column aliases
166 990
        $tables = [];
167 990
        foreach ($tokens['from'] as $table) {
168 990
            $tables[] = $this->name($params, $q, $table, true);
169
        }
170 990
        foreach ($tokens['join'] as $join) {
171 132
            $this->name($params, $q, $join['outer'], true);
172
        }
173
174 990
        return sprintf(
175 990
            "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s",
176 990
            $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
177 990
            $this->columns($params, $q, $tokens['columns']),
178 990
            \implode(', ', $tables),
179 990
            $this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
180 990
            $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
181 990
            $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '),
182 990
            $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])),
183 990
            $this->optional("\n", $this->unions($params, $q, $tokens['union'])),
184 990
            $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
185 990
            $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])),
186 990
            $this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : '')
187
        );
188
    }
189
190 1102
    protected function distinct(QueryParameters $params, Quoter $q, string|bool|array $distinct): string
191
    {
192 1102
        return $distinct === false ? '' : 'DISTINCT';
0 ignored issues
show
introduced by
The condition $distinct === false is always false.
Loading history...
193
    }
194
195 1440
    protected function joins(QueryParameters $params, Quoter $q, array $joins): string
196
    {
197 1440
        $statement = '';
198 1440
        foreach ($joins as $join) {
199 202
            $statement .= sprintf(
200 202
                "\n%s JOIN %s",
201 202
                $join['type'],
202 202
                $this->name($params, $q, $join['outer'], true)
203
            );
204
205 202
            if ($join['alias'] !== null) {
206 74
                $q->registerAlias($join['alias'], (string)$join['outer']);
207
208 74
                $statement .= ' AS ' . $this->name($params, $q, $join['alias']);
209
            }
210
211 202
            $statement .= $this->optional(
212 202
                "\n    ON",
213 202
                $this->where($params, $q, $join['on'])
214
            );
215
        }
216
217 1440
        return $statement;
218
    }
219
220 1440
    protected function unions(QueryParameters $params, Quoter $q, array $unions): string
221
    {
222 1440
        if ($unions === []) {
223 1440
            return '';
224
        }
225
226 24
        $statement = '';
227 24
        foreach ($unions as $union) {
228 24
            $select = $this->fragment($params, $q, $union[1]);
229
230 24
            if ($union[0] !== '') {
231
                //First key is union type, second united query (no need to share compiler)
232 16
                $statement .= "\nUNION {$union[0]}\n{$select}";
233
            } else {
234
                //No extra space
235 16
                $statement .= "\nUNION \n{$select}";
236
            }
237
        }
238
239 24
        return \ltrim($statement, "\n");
240
    }
241
242 1440
    protected function orderBy(QueryParameters $params, Quoter $q, array $orderBy): string
243
    {
244 1440
        $result = [];
245 1440
        foreach ($orderBy as $order) {
246 108
            $direction = \strtoupper($order[1]);
247
248 108
            \in_array($direction, ['ASC', 'DESC']) or throw new CompilerException(
249
                'Invalid sorting direction, only ASC and DESC are allowed'
250
            );
251
252 108
            $result[] = $this->name($params, $q, $order[0]) . ' ' . $direction;
253
        }
254
255 1440
        return \implode(', ', $result);
256
    }
257
258 1440
    protected function groupBy(QueryParameters $params, Quoter $q, array $groupBy): string
259
    {
260 1440
        $result = [];
261 1440
        foreach ($groupBy as $identifier) {
262 80
            $result[] = $this->name($params, $q, $identifier);
263
        }
264
265 1440
        return \implode(', ', $result);
266
    }
267
268
    abstract protected function limit(
269
        QueryParameters $params,
270
        Quoter $q,
271
        int $limit = null,
272
        int $offset = null
273
    ): string;
274
275 104
    protected function updateQuery(
276
        QueryParameters $parameters,
277
        Quoter $quoter,
278
        array $tokens
279
    ): string {
280 104
        $values = [];
281 104
        foreach ($tokens['values'] as $column => $value) {
282 104
            $values[] = sprintf(
283 104
                '%s = %s',
284 104
                $this->name($parameters, $quoter, $column),
285 104
                $this->value($parameters, $quoter, $value)
286
            );
287
        }
288
289 104
        return sprintf(
290 104
            "UPDATE %s\nSET %s%s",
291 104
            $this->name($parameters, $quoter, $tokens['table'], true),
292 104
            trim(implode(', ', $values)),
293 104
            $this->optional("\nWHERE", $this->where($parameters, $quoter, $tokens['where']))
294
        );
295
    }
296
297
    /**
298
     * @psalm-return non-empty-string
299
     */
300 56
    protected function deleteQuery(
301
        QueryParameters $parameters,
302
        Quoter $quoter,
303
        array $tokens
304
    ): string {
305 56
        return sprintf(
306 56
            'DELETE FROM %s%s',
307 56
            $this->name($parameters, $quoter, $tokens['table'], true),
308 56
            $this->optional(
309 56
                "\nWHERE",
310 56
                $this->where($parameters, $quoter, $tokens['where'])
311
            )
312
        );
313
    }
314
315
    /**
316
     * @psalm-return non-empty-string
317
     */
318 1670
    protected function name(QueryParameters $params, Quoter $q, $name, bool $table = false): string
319
    {
320 1670
        if ($name instanceof FragmentInterface) {
321 184
            return $this->fragment($params, $q, $name);
322
        }
323
324 1670
        if ($name instanceof ParameterInterface) {
325 8
            return $this->value($params, $q, $name);
326
        }
327
328 1670
        return $q->quote($name, $table);
329
    }
330
331
    /**
332
     * @psalm-return non-empty-string
333
     */
334 1546
    protected function columns(QueryParameters $params, Quoter $q, array $columns, int $maxLength = 180): string
335
    {
336
        // let's quote every identifier
337 1546
        $columns = array_map(
338 1546
            function ($column) use ($params, $q) {
339 1546
                return $this->name($params, $q, $column);
340 1546
            },
341
            $columns
342
        );
343
344 1546
        return wordwrap(implode(', ', $columns), $maxLength);
345
    }
346
347
    /**
348
     * @psalm-return non-empty-string
349
     */
350 338
    protected function value(QueryParameters $params, Quoter $q, $value): string
351
    {
352 338
        if ($value instanceof FragmentInterface) {
353 16
            return $this->fragment($params, $q, $value);
354
        }
355
356 338
        if (!$value instanceof ParameterInterface) {
357 330
            $value = new Parameter($value);
358
        }
359
360 338
        if ($value->isArray()) {
361 256
            $values = [];
362 256
            foreach ($value->getValue() as $child) {
363 256
                $values[] = $this->value($params, $q, $child);
364
            }
365
366 256
            return '(' . implode(', ', $values) . ')';
367
        }
368
369 338
        $params->push($value);
370
371 338
        return '?';
372
    }
373
374 1560
    protected function where(QueryParameters $params, Quoter $q, array $tokens): string
375
    {
376 1560
        if ($tokens === []) {
377 1496
            return '';
378
        }
379
380 1194
        $statement = '';
381
382 1194
        $activeGroup = true;
383 1194
        foreach ($tokens as $condition) {
384
            // OR/AND keyword
385 1194
            [$boolean, $context] = $condition;
386
387
            // first condition in group/query, no any AND, OR required
388 1194
            if ($activeGroup) {
389
                // next conditions require AND or OR
390 1194
                $activeGroup = false;
391
            } else {
392 480
                $statement .= $boolean;
393 480
                $statement .= ' ';
394
            }
395
396
            /*
397
             * When context is string it usually represent control keyword/syntax such as opening
398
             * or closing braces.
399
             */
400 1194
            if (\is_string($context)) {
401 240
                if ($context === '(') {
402
                    // new where group.
403 240
                    $activeGroup = true;
404
                }
405
406 240
                $statement .= $context;
407 240
                continue;
408
            }
409
410 1186
            if ($context instanceof FragmentInterface) {
411 8
                $statement .= $this->fragment($params, $q, $context);
412 8
                $statement .= ' ';
413 8
                continue;
414
            }
415
416
            // identifier can be column name, expression or even query builder
417 1186
            $statement .= $this->name($params, $q, $context[0]);
418 1186
            $statement .= ' ';
419 1186
            $statement .= $this->condition($params, $q, $context);
420 1186
            $statement .= ' ';
421
        }
422
423 1194
        $activeGroup and throw new CompilerException('Unable to build where statement, unclosed where group');
424
425 1194
        if (trim($statement, ' ()') === '') {
426 8
            return '';
427
        }
428
429 1186
        return $statement;
430
    }
431
432
    /**
433
     * @psalm-return non-empty-string
434
     */
435 1186
    protected function condition(QueryParameters $params, Quoter $q, array $context): string
436
    {
437 1186
        $operator = $context[1];
438 1186
        $value = $context[2];
439
440 1186
        if ($operator instanceof FragmentInterface) {
441 16
            $operator = $this->fragment($params, $q, $operator);
442 1170
        } elseif (!\is_string($operator)) {
443
            throw new CompilerException('Invalid operator type, string or fragment is expected');
444
        }
445
446 1186
        if ($value instanceof FragmentInterface) {
447 308
            return $operator . ' ' . $this->fragment($params, $q, $value);
448
        }
449
450 1040
        if (!$value instanceof ParameterInterface) {
451
            throw new CompilerException('Invalid value format, fragment or parameter is expected');
452
        }
453
454 1040
        $placeholder = '?';
455 1040
        if ($value->isArray()) {
456 50
            if ($operator === '=') {
457
                $operator = 'IN';
458 50
            } elseif ($operator === '!=') {
459
                $operator = 'NOT IN';
460
            }
461
462 50
            $placeholder = '(' . rtrim(str_repeat('? ,', count($value->getValue())), ', ') . ')';
463 50
            $params->push($value);
464 1022
        } elseif ($value->isNull()) {
465 32
            if ($operator === '=') {
466 8
                $operator = 'IS';
467 24
            } elseif ($operator === '!=') {
468 8
                $operator = 'IS NOT';
469
            }
470
471 32
            $placeholder = 'NULL';
472
        } else {
473 990
            $params->push($value);
474
        }
475
476 1040
        if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') {
477 64
            $params->push($context[3]);
478
479
            // possibly support between nested queries
480 64
            return $operator . ' ? AND ?';
481
        }
482
483 976
        return $operator . ' ' . $placeholder;
484
    }
485
486
    /**
487
     * Combine expression with prefix/postfix (usually SQL keyword) but only if expression is not
488
     * empty.
489
     */
490 1560
    protected function optional(string $prefix, string $expression, string $postfix = ''): string
491
    {
492 1560
        if ($expression === '') {
493 1496
            return '';
494
        }
495
496 1292
        if ($prefix !== "\n" && $prefix !== ' ') {
497 1236
            $prefix .= ' ';
498
        }
499
500 1292
        return $prefix . $expression . $postfix;
501
    }
502
}
503