Completed
Branch feature/pre-split (80980a)
by Anton
04:00
created

QueryCompiler::orderParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 5
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Database\Entities;
10
11
use Spiral\Database\Builders\QueryBuilder;
12
use Spiral\Database\Exceptions\CompilerException;
13
use Spiral\Database\Injections\ExpressionInterface;
14
use Spiral\Database\Injections\FragmentInterface;
15
use Spiral\Database\Injections\ParameterInterface;
16
17
/**
18
 * Responsible for conversion of set of query parameters (where tokens, table names and etc) into
19
 * sql to be send into specific Driver.
20
 *
21
 * Source of Compiler must be optimized in nearest future.
22
 */
23
class QueryCompiler
24
{
25
    /**
26
     * Query types for parameter ordering.
27
     */
28
    const SELECT_QUERY = 'select';
29
    const UPDATE_QUERY = 'update';
30
    const DELETE_QUERY = 'delete';
31
    const INSERT_QUERY = 'insert';
32
33
    /**
34
     * Quotes names and expressions.
35
     *
36
     * @var Quoter
37
     */
38
    private $quoter = null;
39
40
    /**
41
     * QueryCompiler constructor.
42
     *
43
     * @param Quoter $quoter
44
     */
45
    public function __construct(Quoter $quoter)
46
    {
47
        $this->quoter = $quoter;
48
    }
49
50
    /**
51
     * Prefix associated with compiler.
52
     *
53
     * @return string
54
     */
55
    public function getPrefix(): string
56
    {
57
        return $this->quoter->getPrefix();
58
    }
59
60
    /**
61
     * Reset table aliases cache, required if same compiler used twice.
62
     *
63
     * @return self
64
     */
65
    public function resetQuoter(): QueryCompiler
66
    {
67
        $this->quoter->reset();
68
69
        return $this;
70
    }
71
72
    /**
73
     * Query query identifier, if identified stated as table - table prefix must be added.
74
     *
75
     * @param string|FragmentInterface $identifier Identifier can include simple column operations
76
     *                                             and functions, having "." in it will
77
     *                                             automatically force table prefix to first value.
78
     * @param bool                     $isTable    Set to true to let quote method know that
79
     *                                             identified is related to table name.
80
     *
81
     * @return string
82
     */
83
    public function quote($identifier, bool $isTable = false): string
84
    {
85
        if ($identifier instanceof FragmentInterface) {
86
            return $this->prepareFragment($identifier);
87
        }
88
89
        return $this->quoter->quote($identifier, $isTable);
90
    }
91
92
    /**
93
     * Create insert query using table names, columns and rowsets. Must support both - single and
94
     * batch inserts.
95
     *
96
     * @param string              $table
97
     * @param array               $columns
98
     * @param FragmentInterface[] $rowsets Every rowset has to be convertable into string. Raw data
99
     *                                     not allowed!
100
     *
101
     * @return string
102
     *
103
     * @throws CompilerException
104
     */
105
    public function compileInsert(string $table, array $columns, array $rowsets): string
106
    {
107
        if (empty($columns)) {
108
            throw new CompilerException(
109
                'Unable to build insert statement, columns must be set'
110
            );
111
        }
112
113
        if (empty($rowsets)) {
114
            throw new CompilerException(
115
                'Unable to build insert statement, at least one value set must be provided'
116
            );
117
        }
118
119
        //To add needed prefixes (if any)
120
        $table = $this->quote($table, true);
121
122
        //Compiling list of columns
123
        $columns = $this->prepareColumns($columns);
124
125
        //Simply joining every rowset
126
        $rowsets = implode(",\n", $rowsets);
127
128
        return "INSERT INTO {$table} ({$columns})\nVALUES {$rowsets}";
129
    }
130
131
    /**
132
     * Create update statement.
133
     *
134
     * @param string $table
135
     * @param array  $updates
136
     * @param array  $whereTokens
137
     *
138
     * @return string
139
     *
140
     * @throws CompilerException
141
     */
142
    public function compileUpdate(string $table, array $updates, array $whereTokens = []): string
143
    {
144
        $table = $this->quote($table, true);
145
146
        //Preparing update column statement
147
        $updates = $this->prepareUpdates($updates);
148
149
        //Where statement is optional for update queries
150
        $whereStatement = $this->optional("\nWHERE", $this->compileWhere($whereTokens));
151
152
        return rtrim("UPDATE {$table}\nSET {$updates} {$whereStatement}");
153
    }
154
155
    /**
156
     * Create delete statement.
157
     *
158
     * @param string $table
159
     * @param array  $whereTokens
160
     *
161
     * @return string
162
     *
163
     * @throws CompilerException
164
     */
165
    public function compileDelete(string $table, array $whereTokens = []): string
166
    {
167
        $table = $this->quote($table, true);
168
169
        //Where statement is optional for delete query (which is weird)
170
        $whereStatement = $this->optional("\nWHERE", $this->compileWhere($whereTokens));
171
172
        return rtrim("DELETE FROM {$table} {$whereStatement}");
173
    }
174
175
    /**
176
     * Create select statement. Compiler must validly resolve table and column aliases used in
177
     * conditions and joins.
178
     *
179
     * @param array       $fromTables
180
     * @param bool|string $distinct String only for PostgresSQL.
181
     * @param array       $columns
182
     * @param array       $joinTokens
183
     * @param array       $whereTokens
184
     * @param array       $havingTokens
185
     * @param array       $grouping
186
     * @param array       $ordering
187
     * @param int         $limit
188
     * @param int         $offset
189
     * @param array       $unionTokens
190
     *
191
     * @return string
192
     *
193
     * @throws CompilerException
194
     */
195
    public function compileSelect(
196
        array $fromTables,
197
        $distinct,
198
        array $columns,
199
        array $joinTokens = [],
200
        array $whereTokens = [],
201
        array $havingTokens = [],
202
        array $grouping = [],
203
        array $ordering = [],
204
        int $limit = 0,
205
        int $offset = 0,
206
        array $unionTokens = []
207
    ): string {
208
        //This statement parts should be processed first to define set of table and column aliases
209
        $fromTables = $this->compileTables($fromTables);
210
211
        $joinsStatement = $this->optional(' ', $this->compileJoins($joinTokens), ' ');
212
213
        //Distinct flag (if any)
214
        $distinct = $this->optional(' ', $this->compileDistinct($distinct));
215
216
        //Columns are compiled after table names and joins to ensure aliases and prefixes
217
        $columns = $this->prepareColumns($columns);
218
219
        //A lot of constrain and other statements
220
        $whereStatement = $this->optional("\nWHERE", $this->compileWhere($whereTokens));
221
        $havingStatement = $this->optional("\nHAVING", $this->compileWhere($havingTokens));
222
        $groupingStatement = $this->optional("\nGROUP BY", $this->compileGrouping($grouping), ' ');
223
224
        //Union statement has new line at beginning of every union
225
        $unionsStatement = $this->optional("\n", $this->compileUnions($unionTokens));
226
        $orderingStatement = $this->optional("\nORDER BY", $this->compileOrdering($ordering));
227
228
        $limingStatement = $this->optional("\n", $this->compileLimit($limit, $offset));
229
230
        //Initial statement have predictable order
231
        $statement = "SELECT{$distinct}\n{$columns}\nFROM {$fromTables}";
232
        $statement .= "{$joinsStatement}{$whereStatement}{$groupingStatement}{$havingStatement}";
233
        $statement .= "{$unionsStatement}{$orderingStatement}{$limingStatement}";
234
235
        return rtrim($statement);
236
    }
237
238
    /**
239
     * Quote and wrap column identifiers (used in insert statement compilation).
240
     *
241
     * @param array $columnIdentifiers
242
     * @param int   $maxLength Automatically wrap columns.
243
     *
244
     * @return string
245
     */
246
    protected function prepareColumns(array $columnIdentifiers, int $maxLength = 180): string
247
    {
248
        //Let's quote every identifier
249
        $columnIdentifiers = array_map([$this, 'quote'], $columnIdentifiers);
250
251
        return wordwrap(implode(', ', $columnIdentifiers), $maxLength);
252
    }
253
254
    /**
255
     * Prepare column values to be used in UPDATE statement.
256
     *
257
     * @param array $updates
258
     *
259
     * @return string
260
     */
261
    protected function prepareUpdates(array $updates): string
262
    {
263
        foreach ($updates as $column => &$value) {
264
            if ($value instanceof FragmentInterface) {
265
                $value = $this->prepareFragment($value);
266
            } else {
267
                //Simple value (such condition should never be met since every value has to be
268
                //wrapped using parameter interface)
269
                $value = '?';
270
            }
271
272
            $value = "{$this->quote($column)} = {$value}";
273
            unset($value);
274
        }
275
276
        return trim(implode(',', $updates));
277
    }
278
279
    /**
280
     * Compile DISTINCT statement.
281
     *
282
     * @param mixed $distinct Not every DBMS support distinct expression, only Postgres does.
283
     *
284
     * @return string
285
     */
286
    protected function compileDistinct($distinct): string
287
    {
288
        if (empty($distinct)) {
289
            return '';
290
        }
291
292
        return 'DISTINCT';
293
    }
294
295
    /**
296
     * Compile table names statement.
297
     *
298
     * @param array $tables
299
     *
300
     * @return string
301
     */
302
    protected function compileTables(array $tables): string
303
    {
304
        foreach ($tables as &$table) {
305
            $table = $this->quote($table, true);
306
            unset($table);
307
        }
308
309
        return implode(', ', $tables);
310
    }
311
312
    /**
313
     * Compiler joins statement.
314
     *
315
     * @param array $joinTokens
316
     *
317
     * @return string
318
     */
319
    protected function compileJoins(array $joinTokens): string
320
    {
321
        $statement = '';
322
        foreach ($joinTokens as $table => $join) {
323
            $statement .= "\n" . $join['type'] . ' JOIN ' . $this->quote($table, true);
324
            $statement .= $this->optional("\n    ON", $this->compileWhere($join['on']));
325
        }
326
327
        return $statement;
328
    }
329
330
    /**
331
     * Compile union statement chunk. Keywords UNION and ALL will be included, this methods will
332
     * automatically move every union on new line.
333
     *
334
     * @param array $unionTokens
335
     *
336
     * @return string
337
     */
338
    protected function compileUnions(array $unionTokens): string
339
    {
340
        if (empty($unionTokens)) {
341
            return '';
342
        }
343
344
        $statement = '';
345
        foreach ($unionTokens as $union) {
346
            if (!empty($union[0])) {
347
                //First key is union type, second united query (no need to share compiler)
348
                $statement .= "\nUNION {$union[0]}\n({$union[1]})";
349
            } else {
350
                //No extra space
351
                $statement .= "\nUNION \n({$union[1]})";
352
            }
353
        }
354
355
        return ltrim($statement, "\n");
356
    }
357
358
    /**
359
     * Compile ORDER BY statement.
360
     *
361
     * @param array $ordering
362
     *
363
     * @return string
364
     */
365
    protected function compileOrdering(array $ordering): string
366
    {
367
        $result = [];
368
        foreach ($ordering as $order) {
369
            $result[] = $this->quote($order[0]) . ' ' . strtoupper($order[1]);
370
        }
371
372
        return implode(', ', $result);
373
    }
374
375
    /**
376
     * Compiler GROUP BY statement.
377
     *
378
     * @param array $grouping
379
     *
380
     * @return string
381
     */
382
    protected function compileGrouping(array $grouping): string
383
    {
384
        $statement = '';
385
        foreach ($grouping as $identifier) {
386
            $statement .= $this->quote($identifier);
387
        }
388
389
        return $statement;
390
    }
391
392
    /**
393
     * Compile limit statement.
394
     *
395
     * @param int $limit
396
     * @param int $offset
397
     *
398
     * @return string
399
     */
400
    protected function compileLimit(int $limit, int $offset): string
401
    {
402
        if (empty($limit) && empty($offset)) {
403
            return '';
404
        }
405
406
        $statement = '';
407
        if (!empty($limit)) {
408
            $statement = "LIMIT {$limit} ";
409
        }
410
411
        if (!empty($offset)) {
412
            $statement .= "OFFSET {$offset}";
413
        }
414
415
        return trim($statement);
416
    }
417
418
    /**
419
     * Compile where statement.
420
     *
421
     * @param array $tokens
422
     *
423
     * @return string
424
     *
425
     * @throws CompilerException
426
     */
427
    protected function compileWhere(array $tokens): string
428
    {
429
        if (empty($tokens)) {
430
            return '';
431
        }
432
433
        $statement = '';
434
435
        $activeGroup = true;
436
        foreach ($tokens as $condition) {
437
            //OR/AND keyword
438
            $boolean = $condition[0];
439
440
            //See AbstractWhere
441
            $context = $condition[1];
442
443
            //First condition in group/query, no any AND, OR required
444
            if ($activeGroup) {
445
                //Kill AND, OR and etc.
1 ignored issue
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
446
                $boolean = '';
447
448
                //Next conditions require AND or OR
1 ignored issue
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
449
                $activeGroup = false;
450
            }
451
452
            /*
453
             * When context is string it usually represent control keyword/syntax such as opening
454
             * or closing braces.
455
             */
456
            if (is_string($context)) {
457
                if ($context == '(') {
458
                    //New where group.
459
                    $activeGroup = true;
460
                }
461
462
                $statement .= ltrim("{$boolean} {$context} ");
463
464
                if ($context == ')') {
465
                    //We don't need trailing space
466
                    $statement = rtrim($statement);
467
                }
468
469
                continue;
470
            }
471
472
            if ($context instanceof FragmentInterface) {
473
                //Fragments has to be compiled separately
474
                $statement .= "{$boolean} {$this->prepareFragment($context)} ";
475
                continue;
476
            }
477
478
            //Now we are operating with "class" where function, where we need 3 variables where(id, =, 1)
479
            if (!is_array($context)) {
480
                throw new CompilerException('Invalid where token, context expected to be an array');
481
            }
482
483
            /*
484
             * This is "normal" where token which includes identifier, operator and value.
485
             */
486
            list($identifier, $operator, $value) = $context;
487
488
            //Identifier can be column name, expression or even query builder
489
            $identifier = $this->quote($identifier);
490
491
            //Value has to be prepared as well
492
            $placeholder = $this->prepareValue($value);
493
494
            if ($operator == 'BETWEEN' || $operator == 'NOT BETWEEN') {
495
                //Between statement has additional parameter
496
                $right = $this->prepareValue($context[3]);
497
498
                $statement .= "{$boolean} {$identifier} {$operator} {$placeholder} AND {$right} ";
499
                continue;
500
            }
501
502
            //Compiler can switch equal to IN if value points to array (do we need it?)
503
            $operator = $this->prepareOperator($value, $operator);
504
505
            $statement .= "{$boolean} {$identifier} {$operator} {$placeholder} ";
506
        }
507
508
        if ($activeGroup) {
509
            throw new CompilerException('Unable to build where statement, unclosed where group');
510
        }
511
512
        return trim($statement);
513
    }
514
515
    /**
516
     * Combine expression with prefix/postfix (usually SQL keyword) but only if expression is not
517
     * empty.
518
     *
519
     * @param string $prefix
520
     * @param string $expression
521
     * @param string $postfix
522
     *
523
     * @return string
524
     */
525
    protected function optional(string $prefix, string $expression, string $postfix = ''): string
526
    {
527
        if (empty($expression)) {
528
            return '';
529
        }
530
531
        if ($prefix != "\n" && $prefix != ' ') {
532
            $prefix .= ' ';
533
        }
534
535
        return $prefix . $expression . $postfix;
536
    }
537
538
    /**
539
     * Resolve operator value based on value value. ;).
540
     *
541
     * @param mixed  $parameter
542
     * @param string $operator
543
     *
544
     * @return string
545
     */
546
    protected function prepareOperator($parameter, string $operator): string
547
    {
548
        if (!$parameter instanceof ParameterInterface) {
549
            //Probably fragment
550
            return $operator;
551
        }
552
553
        if ($operator != '=' || is_scalar($parameter->getValue())) {
554
            //Doing nothing for non equal operators
555
            return $operator;
556
        }
557
558
        if ($parameter->isArray()) {
559
            //Automatically switching between equal and IN
560
            return 'IN';
561
        }
562
563
        return $operator;
564
    }
565
566
    /**
567
     * Prepare value to be replaced into query (replace ?).
568
     *
569
     * @param mixed $value
570
     *
571
     * @return string
572
     */
573
    protected function prepareValue($value): string
574
    {
575
        if ($value instanceof FragmentInterface) {
576
            return $this->prepareFragment($value);
577
        }
578
579
        //Technically should never happen (but i prefer to keep this legacy code)
580
        return '?';
581
    }
582
583
    /**
584
     * Prepare where fragment to be injected into statement.
585
     *
586
     * @param FragmentInterface $context
587
     *
588
     * @return string
589
     */
590
    protected function prepareFragment(FragmentInterface $context): string
591
    {
592
        if ($context instanceof QueryBuilder) {
593
            //Nested queries has to be wrapped with braces
594
            return '(' . $context->sqlStatement($this) . ')';
595
        }
596
597
        if ($context instanceof ExpressionInterface) {
598
            //Fragments does not need braces around them
599
            return $context->sqlStatement($this);
600
        }
601
602
        return $context->sqlStatement();
603
    }
604
}
605