Issues (33)

src/QueryBuilder.php (3 issues)

1
<?php
2
3
namespace Tsukasa\QueryBuilder;
4
5
use Doctrine\DBAL\Driver\Connection;
6
use Tsukasa\QueryBuilder\Aggregation\Aggregation;
7
use Tsukasa\QueryBuilder\Exception\QBException;
8
use Tsukasa\QueryBuilder\Expression\Expression;
9
use Tsukasa\QueryBuilder\Interfaces\IAdapter;
10
use Tsukasa\QueryBuilder\Interfaces\ILookupBuilder;
11
use Tsukasa\QueryBuilder\Interfaces\ILookupCollection;
12
use Tsukasa\QueryBuilder\Interfaces\ISQLGenerator;
13
use Tsukasa\QueryBuilder\Interfaces\IToSql;
14
use Tsukasa\QueryBuilder\Interfaces\QueryBuilderInterface;
15
use Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder;
16
use Tsukasa\QueryBuilder\Q\Q;
17
18
use Tsukasa\QueryBuilder\Database\Mysql\Adapter as MysqlAdapter;
19
use Tsukasa\QueryBuilder\Database\Sqlite\Adapter as SqliteAdapter;
20
use Tsukasa\QueryBuilder\Database\Pgsql\Adapter as PgsqlAdapter;
21
22
class QueryBuilder implements QueryBuilderInterface
23
{
24
    const TYPE_SELECT = 'SELECT';
25
    const TYPE_INSERT = 'INSERT';
26
    const TYPE_UPDATE = 'UPDATE';
27
    const TYPE_DELETE = 'DELETE';
28
29
    /**
30
     * @var array|Q|string
31
     */
32
    private $_whereAnd = [];
33
    /**
34
     * @var array|Q|string
35
     */
36
    private $_whereOr = [];
37
    /**
38
     * @var array|string
39
     */
40
    private $_join = [];
41
    /**
42
     * @var array|string
43
     */
44
    private $_order = [];
45
    /**
46
     * @var array
47
     */
48
    private $_group = [];
49
    /**
50
     * @var array|string|\Tsukasa\QueryBuilder\Aggregation\Aggregation
51
     */
52
    private $_select = [];
53
    /**
54
     * @var array|string|null
55
     */
56
    private $_from;
57
    /**
58
     * @var array
59
     */
60
    private $_union = [];
61
    /**
62
     * @var null|int
63
     */
64
    private $_limit;
65
    /**
66
     * @var null|int
67
     */
68
    private $_offset;
69
    /**
70
     * @var array
71
     */
72
    private $_having = [];
73
    /**
74
     * @var null|string
75
     */
76
    private $_alias;
77
    /**
78
     * @var null|string sql query type SELECT|UPDATE|DELETE
79
     */
80
    private $_type;
81
    /**
82
     * @var array
83
     */
84
    private $_update = [];
85
    /**
86
     * @var ISQLGenerator
87
     */
88
    protected $adapter;
89
    /**
90
     * @var ILookupBuilder
91
     */
92
    protected $lookupBuilder;
93
    /**
94
     * @var null
95
     */
96
    protected $schema;
97
    /**
98
     * Counter of joined tables aliases
99
     * @var int
100
     */
101
    private $_aliasesCount = 0;
102
103
    private $_joinAlias = [];
104
105
    /**
106
     * Strings options query
107
     * @var string
108
     */
109
    private $_queryOptions = '';
110
    /**
111
     * @var Connection
112
     */
113
    protected $connection;
114
115
    public function getConnection()
116
    {
117
        return $this->connection;
118
    }
119
120
    /**
121
     * @param  $connection \Doctrine\DBAL\Connection
122
     */
123
    public function setConnection($connection)
124
    {
125
        $this->connection = $connection;
126
    }
127
128
    /**
129
     * @param Connection $connection
130
     * @param ISQLGenerator|null $adapter
131
     * @param LookupBuilder|null $lookupBuilder
132
     * @return QueryBuilderInterface
133
     */
134
    public static function getInstance(Connection $connection, $adapter = null, $lookupBuilder = null)
135
    {
136
        if ($adapter === null) {
137
            $driver = $connection->getDriver();
0 ignored issues
show
The method getDriver() does not exist on Doctrine\DBAL\Driver\Connection. It seems like you code against a sub-type of Doctrine\DBAL\Driver\Connection such as Doctrine\DBAL\Connection. ( Ignorable by Annotation )

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

137
            /** @scrutinizer ignore-call */ 
138
            $driver = $connection->getDriver();
Loading history...
138
            switch ($driver->getName()) {
139
                case 'pdo_mysql':
140
                    $adapter = new MysqlAdapter($connection);
141
                    break;
142
                case 'pdo_sqlite':
143
                    $adapter = new SqliteAdapter($connection);
144
                    break;
145
                case 'pdo_pgsql':
146
                    $adapter = new PgsqlAdapter($connection);
147
                    break;
148
                default:
149
                    throw new QBException('Unknown driver');
150
            }
151
        }
152
153
        $lookupBuilder = $lookupBuilder ?: new LookupBuilder();
154
        $lookupBuilder->addLookupCollection($adapter->getLookupCollection());
155
        return new static($connection, $adapter, $lookupBuilder);
156
    }
157
158
    /**
159
     * QueryBuilder constructor.
160
     * @param Connection $connection
161
     * @param ISQLGenerator $adapter
162
     * @param ILookupBuilder $lookupBuilder
163
     */
164
    public function __construct(Connection $connection, ISQLGenerator $adapter, ILookupBuilder $lookupBuilder)
165
    {
166
        $this->connection = $connection;
167
        $this->adapter = $adapter;
168
        $this->lookupBuilder = $lookupBuilder;
169
    }
170
171
    /**
172
     * @param ILookupCollection $lookupCollection
173
     * @return QueryBuilderInterface
174
     */
175
    public function addLookupCollection(ILookupCollection $lookupCollection)
176
    {
177
        $this->lookupBuilder->addLookupCollection($lookupCollection);
178
        return $this;
179
    }
180
181
    public function setType($type)
182
    {
183
        $types = [static::TYPE_INSERT, static::TYPE_UPDATE, static::TYPE_DELETE, static::TYPE_SELECT];
184
        if (in_array($type, $types, true)) {
185
            $this->_type = $type;
186
        }
187
        else {
188
            throw new QBException('Incorrect type');
189
        }
190
191
192
        return $this;
193
    }
194
195
    /**
196
     * @return QueryBuilderInterface
197
     */
198
    public function setTypeSelect()
199
    {
200
        $this->_type = self::TYPE_SELECT;
201
        return $this;
202
    }
203
204
    /**
205
     * @return QueryBuilderInterface
206
     */
207
    public function setTypeInsert()
208
    {
209
        $this->_type = self::TYPE_INSERT;
210
        return $this;
211
    }
212
213
    /**
214
     * @return QueryBuilderInterface
215
     */
216
    public function setTypeUpdate()
217
    {
218
        $this->_type = self::TYPE_UPDATE;
219
        return $this;
220
    }
221
222
    /**
223
     * @return QueryBuilderInterface
224
     */
225
    public function setTypeDelete()
226
    {
227
        $this->_type = self::TYPE_DELETE;
228
        return $this;
229
    }
230
231
    /**
232
     * If type is null return TYPE_SELECT
233
     * @return string
234
     */
235
    public function getType()
236
    {
237
        return $this->_type === null
238
            ? self::TYPE_SELECT
239
            : $this->_type;
240
    }
241
242
    public function setOptions($options = '')
243
    {
244
        $this->_queryOptions = $options;
245
        return $this;
246
    }
247
248
    /**
249
     * @param Aggregation $aggregation
250
     * @return string
251
     */
252
    protected function buildSelectFromAggregation(Aggregation $aggregation)
253
    {
254
        $tableAlias = $this->getAlias();
255
        $rawColumn = $aggregation->getField();
256
        $newSelect = $this->getLookupBuilder()->buildJoin($this, $rawColumn);
0 ignored issues
show
The method buildJoin() does not exist on Tsukasa\QueryBuilder\Interfaces\ILookupBuilder. It seems like you code against a sub-type of Tsukasa\QueryBuilder\Interfaces\ILookupBuilder such as Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder. ( Ignorable by Annotation )

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

256
        $newSelect = $this->getLookupBuilder()->/** @scrutinizer ignore-call */ buildJoin($this, $rawColumn);
Loading history...
257
        if ($newSelect === false) {
258
            if ($tableAlias === null || $rawColumn === '*') {
259
                $columns = $rawColumn;
260
            }
261
            elseif (strpos($rawColumn, '.') !== false) {
262
                $columns = $rawColumn;
263
            }
264
            else {
265
                $columns = $tableAlias . '.' . $rawColumn;
266
            }
267
        }
268
        else {
269
            list($alias, $joinColumn) = $newSelect;
270
            $columns = $alias . '.' . $joinColumn;
271
        }
272
        $fieldsSql = $this->getAdapter()->buildColumns($columns);
273
        $aggregation->setFieldSql($fieldsSql);
274
275
        return $aggregation->setQB($this)->toSQL();
276
    }
277
278
    /**
279
     * @return string
280
     * @throws \Exception
281
     */
282
    protected function buildSelect()
283
    {
284
        if (empty($this->_select)) {
285
            $this->_select = ['*'];
286
        }
287
288
        $select = [];
289
        $builder = $this->getLookupBuilder();
290
        if (is_array($this->_select)) {
291
            foreach ($this->_select as $alias => $column) {
292
                if ($column instanceof Aggregation) {
293
                    $select[$alias] = $this->buildSelectFromAggregation($column);
294
                }
295
                else if (is_string($column)) {
296
                    if (strpos($column, 'SELECT') !== false) {
297
                        $select[$alias] = $column;
298
                    }
299
                    else {
300
                        $select[$alias] = $this->addColumnAlias($builder->fetchColumnName($column));
301
                    }
302
                }
303
                else {
304
                    $select[$alias] = $column;
305
                }
306
            }
307
        }
308
        else if (is_string($this->_select)) {
309
            $select = $this->addColumnAlias($this->_select);
310
        }
311
        return $this->getAdapter()->sqlSelect($select, $this->_queryOptions);
312
    }
313
314
    protected function pushToSelect($select, $alias = null)
315
    {
316
        $isValid = is_string($select)
317
            || (is_numeric($select) && is_finite($select))
318
            || is_a($select, Expression::class)
319
            || is_a($select, Aggregation::class)
320
        ;
321
322
        if (!$isValid) {
323
            throw new QBException('Incorrect select type');
324
        }
325
326
        if ($alias) {
327
            $this->_select[$alias] = $select;
328
        }
329
        else {
330
            $this->_select[] = $select;
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * @param string|IToSql $select
338
     * @param null $alias
339
     * @return QueryBuilderInterface
340
     */
341
    public function addSelect($select, $alias = null)
342
    {
343
        if (is_string($select) && $newSelect = $this->getLookupBuilder()->buildJoin($this, $select)) {
344
            list($t_alias, $column) = $newSelect;
345
            $this->pushToSelect($t_alias . '.' . $column, $alias);
346
        }
347
        else {
348
            $this->pushToSelect(
349
                $this->hydrate($select),
350
                $alias
351
            );
352
        }
353
354
        return $this;
355
    }
356
357
    /**
358
     * @param array|string $select
359
     * @return QueryBuilderInterface
360
     */
361
    public function setSelect($select)
362
    {
363
        $this->_select = [];
364
365
        if (empty($select)) {
366
            return $this;
367
        }
368
369
        if (is_array($select)) {
370
            foreach ($select as $key => $part) {
371
                $this->addSelect($part, $key);
372
            }
373
        }
374
        else {
375
            $this->addSelect($select);
376
        }
377
378
        return $this;
379
    }
380
381
    /**
382
     * @param array|string $tableName
383
     * @param null|string $alias
384
     * @return QueryBuilderInterface
385
     */
386
    public function setFrom($tableName, $alias = null)
387
    {
388
        if ($alias && is_string($alias)) {
389
            if (is_array($tableName)) {
390
                $tableName = current($tableName);
391
            }
392
393
            $tableName = [$alias => $tableName];
394
        }
395
396
        $this->_from = $tableName;
397
        return $this;
398
    }
399
400
    /**
401
     * @param $alias string join alias
402
     * @return bool
403
     */
404
    public function hasJoin($alias)
405
    {
406
        return array_key_exists($alias, $this->_join);
407
    }
408
409
    /**
410
     * @param int $page
411
     * @param int $pageSize
412
     * @return QueryBuilderInterface
413
     */
414
    public function paginate($page = 1, $pageSize = 10)
415
    {
416
        $page = (int)$page;
417
        $pageSize = (int)$pageSize;
418
419
        $this->setLimit($pageSize);
420
        $this->setOffset($page > 1 ? $pageSize * ($page - 1) : 0);
421
        return $this;
422
    }
423
424
    /**
425
     * @param string|number $limit
426
     * @return QueryBuilderInterface
427
     */
428
    public function setLimit($limit)
429
    {
430
        $this->_limit = (int)$limit;
431
        return $this;
432
    }
433
434
    /**
435
     * @return int
436
     */
437
    public function getLimit()
438
    {
439
        return $this->_limit;
440
    }
441
442
    /**
443
     * @param string|number $offset
444
     * @return QueryBuilderInterface
445
     */
446
    public function setOffset($offset)
447
    {
448
        $this->_offset = (int)$offset;
449
        return $this;
450
    }
451
452
    /**
453
     * @return int|string|null
454
     */
455
    public function getOffset()
456
    {
457
        return $this->_offset;
458
    }
459
460
    /**
461
     * @return ILookupBuilder|\Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder
462
     */
463
    public function getLookupBuilder()
464
    {
465
        return $this->lookupBuilder;
466
    }
467
468
    /**
469
     * @return ISQLGenerator
470
     */
471
    public function getAdapter()
472
    {
473
        return $this->adapter;
474
    }
475
476
    /**
477
     * @param string $joinType LEFT JOIN, RIGHT JOIN, etc...
478
     * @param string|QueryBuilderInterface $tableName
479
     * @param array $on link columns
480
     * @param string|null $alias string
481
     * @param string|null $index
482
     * @return QueryBuilderInterface
483
     */
484
    public function join($joinType, $tableName, array $on = [], $alias = null, $index = null)
485
    {
486
        if ($tableName instanceof QueryBuilderInterface) {
487
            $this->_join[] = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias, $index);
488
        }
489
        else {
490
            if ($joinType === 'RAW' && !empty($tableName)) {
491
                $join = $this->getAdapter()->quoteSql($tableName);
492
            }
493
            else {
494
                $join = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias);
495
            }
496
497
            if (!$alias) {
498
                $alias = count($this->_join);
0 ignored issues
show
It seems like $this->_join can also be of type string; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

498
                $alias = count(/** @scrutinizer ignore-type */ $this->_join);
Loading history...
499
            }
500
            $this->_join[$alias] = $join;
501
            $this->_joinAlias[$tableName][] = $alias;
502
        }
503
        return $this;
504
    }
505
506
    /**
507
     * @param $sql
508
     * @param string $alias
509
     * @return QueryBuilderInterface
510
     */
511
    public function joinRaw($sql, $alias = null)
512
    {
513
        return $this->join('RAW', $sql, [], $alias);
514
    }
515
516
    /**
517
     * @param array|string $columns columns
518
     * @return QueryBuilderInterface
519
     */
520
    public function setGroup($columns)
521
    {
522
        if ($columns && is_string($columns)) {
523
            $columns = array_map('trim', explode(',', $columns));
524
        }
525
526
        $this->_group = $columns;
527
        return $this;
528
    }
529
530
    /**
531
     * @param array|string $columns columns
532
     * @return QueryBuilderInterface
533
     */
534
    public function addGroup($columns)
535
    {
536
        if ($columns && is_string($columns)) {
537
            $columns = array_map('trim', explode(',', $columns));
538
        }
539
        $this->_group = array_merge($this->_group, $columns);
540
        return $this;
541
    }
542
543
    protected function pushOrder($column)
544
    {
545
546
    }
547
548
    /**
549
     * @param array|string|null $columns columns
550
     * @return QueryBuilderInterface
551
     */
552
    public function setOrder($columns)
553
    {
554
555
        $this->_order = [];
556
557
        if (empty($columns)) {
558
            return $this;
559
        }
560
561
        if (is_array($columns)) {
562
            foreach ($columns as $column) {
563
                $this->addOrder($column);
564
            }
565
        }
566
        else {
567
            $this->addOrder($columns);
568
        }
569
570
        return $this;
571
    }
572
573
    /**
574
     * @param string|Expression $column
575
     * @return QueryBuilderInterface
576
     */
577
    public function addOrder($column)
578
    {
579
        $isValid = is_string($column)
580
            || is_a($column, Expression::class)
581
        ;
582
583
        if (!$isValid) {
584
            throw new QBException('Incorrect order type');
585
        }
586
587
        if (is_string($column) && strpos($column, ',') !== false) {
588
            $columns = preg_split('/\s*,\s*/', $column, -1, PREG_SPLIT_NO_EMPTY);
589
590
            foreach ($columns as $_column) {
591
                $temp = explode(' ', $_column);
592
                if (count($temp) === 2) {
593
594
                    $_column = $temp[0];
595
                    if (strtoupper(trim($temp[1])) === 'DESC') {
596
                        $_column = '-' . $_column;
597
                    }
598
599
                    $this->_order[] = $_column;
600
                }
601
                else {
602
                    $this->_order[] = current($temp);
603
                }
604
            }
605
        }
606
        else {
607
            $this->_order[] = $column;
608
        }
609
610
        return $this;
611
    }
612
613
    /**
614
     * @param $tableName
615
     * @param array $rows
616
     * @return QueryBuilderInterface
617
     */
618
    public function insert($tableName, $rows)
619
    {
620
        $this->setTypeInsert();
621
        $this->_update = [$tableName, $rows];
622
        return $this;
623
    }
624
625
    /**
626
     * @param $tableName string
627
     * @param array $values columns [name => value...]
628
     * @return QueryBuilderInterface
629
     */
630
    public function update($tableName, array $values)
631
    {
632
        $this->setTypeUpdate();
633
        $this->_update = [$tableName, $values];
634
        return $this;
635
    }
636
637
    public function getAlias()
638
    {
639
        return $this->_alias;
640
    }
641
642
    public function setAlias($alias = null)
643
    {
644
        if (empty($alias)) {
645
            $alias = null;
646
        }
647
648
        $this->_alias = $alias;
649
        return $this;
650
    }
651
652
    protected function buildCondition($condition, &$params = [])
653
    {
654
        if (!is_array($condition)) {
655
            return (string)$condition;
656
        }
657
658
        if (empty($condition)) {
659
            return '';
660
        }
661
662
        if (isset($condition[0]) && is_string($condition[0])) {
663
            $operatorRaw = array_shift($condition);
664
            $operator = strtoupper($operatorRaw);
665
            return $this->buildAndCondition($operator, $condition, $params);
666
        }
667
668
        return $this->parseCondition($condition);
669
    }
670
671
    public function getJoinAlias($tableName)
672
    {
673
        return $this->_joinAlias[$tableName];
674
    }
675
676
    public function getJoins()
677
    {
678
        return $this->_join;
679
    }
680
681
    /**
682
     * @param $condition
683
     * @param string $operator
684
     * @return string
685
     */
686
    public function parseCondition($condition, $operator = 'AND')
687
    {
688
        $parts = [];
689
690
        if (is_array($condition)) {
691
            foreach ($condition as $key => $value) {
692
                if (is_numeric($key)) {
693
                    if ($value instanceof IToSql) {
694
                        $parts[] = $this->parseCondition($value, $operator);
695
                    }
696
                    elseif ($value instanceof QueryBuilder) {
697
                        $parts[] = $this->parseCondition($value, $operator);
698
                    }
699
                    else if (is_array($value)) {
700
                        $parts[] = $this->parseCondition($value, $operator);
701
                    }
702
                    else if (is_string($value)) {
703
                        $parts[] = $value;
704
                    }
705
                }
706
                else {
707
                    $tableAlias = $this->getAlias();
708
                    $value = $this->getAdapter()->prepareValue($value);
709
710
                    list($lookup, $column, $lookupValue) = $this->lookupBuilder->parseLookup($this, $key, $value);
711
                    $column = $this->getLookupBuilder()->fetchColumnName($column);
712
                    if ($tableAlias !== null && strpos($column, '.') === false) {
713
                        $column = $tableAlias . '.' . $column;
714
                    }
715
                    $parts[] = $this->lookupBuilder->runLookup($this->getAdapter(), $lookup, $column, $lookupValue);
716
                }
717
            }
718
719
            if ($parts) {
720
                if (count($parts) === 1) {
721
                    return $parts[0];
722
                }
723
724
                return '(' . implode(') ' . $operator . ' (', $parts) . ')';
725
            }
726
727
        }
728
        else if ($condition instanceof IToSql) {
729
            return $condition
730
                ->setQb($this)
731
                ->toSql();
732
        }
733
        else if ($condition instanceof QueryBuilder) {
734
            return $condition->toSQL();
735
        }
736
        else if (is_string($condition)) {
737
            return $condition;
738
        }
739
740
        return '';
741
    }
742
743
    protected function buildAndCondition($operator, $operands, &$params)
744
    {
745
        $parts = [];
746
        foreach ($operands as $operand) {
747
            if (is_array($operand)) {
748
                $operand = $this->buildCondition($operand, $params);
749
            }
750
            else {
751
                $operand = $this->parseCondition($operand);
752
            }
753
            if ($operand !== '') {
754
                $parts[] = $this->getAdapter()->quoteSql($operand);
755
            }
756
        }
757
        if (!empty($parts)) {
758
            return '(' . implode(') ' . $operator . ' (', $parts) . ')';
759
        }
760
761
        return '';
762
    }
763
764
    /**
765
     * @param $condition
766
     * @return QueryBuilderInterface
767
     */
768
    public function addWhere($condition)
769
    {
770
        if (!empty($condition)) {
771
            $this->_whereAnd[] = $this->hydrate($condition);
772
        }
773
        return $this;
774
    }
775
776
    public function setWhere($condition)
777
    {
778
        $this->_whereAnd = [];
779
780
        return $this->addWhere($condition);
781
    }
782
783
    /**
784
     * @param $condition
785
     * @return QueryBuilderInterface
786
     */
787
    public function addOrWhere($condition)
788
    {
789
        if (!empty($condition)) {
790
            $this->_whereOr[] = $this->hydrate($condition);
791
        }
792
        return $this;
793
    }
794
795
    public function setOrWhere($condition)
796
    {
797
        $this->_whereOr = [];
798
799
        return $this->addWhere($condition);
800
    }
801
802
    /**
803
     * @return array
804
     */
805
    protected function buildWhereTree()
806
    {
807
        $where = [];
808
        foreach ($this->_whereAnd as $condition) {
809
            if (empty($where)) {
810
                $where = ['and', $condition];
811
            }
812
            else {
813
                $where = ['and', $where, ['and', $condition]];
814
            }
815
        }
816
817
        foreach ($this->_whereOr as $condition) {
818
            if (empty($where)) {
819
                $where = ['or', $condition];
820
            }
821
            else {
822
                $where = ['or', $where, ['and', $condition]];
823
            }
824
        }
825
        return $where;
826
    }
827
828
    public function getSelect()
829
    {
830
        return $this->_select;
831
    }
832
833
    protected function buildWhere()
834
    {
835
        $params = [];
836
        $sql = $this->buildCondition($this->buildWhereTree(), $params);
837
        return empty($sql) ? '' : ' WHERE ' . $sql;
838
    }
839
840
841
    protected function generateSelectSql()
842
    {
843
        // Fetch where conditions before pass it to adapter.
844
        // Reason: Dynamic sql build in callbacks
845
846
        // $this->prepareJoin();
847
848
        $where = $this->buildWhere();
849
        $order = $this->buildOrder();
850
        $union = $this->buildUnion();
851
852
        $select = $this->buildSelect();
853
        $from = $this->buildFrom();
854
        $join = $this->buildJoin();
855
        $group = $this->buildGroup();
856
        $having = $this->buildHaving();
857
        $limitOffset = $this->buildLimitOffset();
858
        return strtr('{select}{from}{join}{where}{group}{having}{order}{limit_offset}{union}', [
859
            '{select}' => $select,
860
            '{from}' => $from,
861
            '{where}' => $where,
862
            '{group}' => $group,
863
            '{order}' => empty($union) ? $order : '',
864
            '{having}' => $having,
865
            '{join}' => $join,
866
            '{limit_offset}' => $limitOffset,
867
            '{union}' => empty($union) ? '' : $union . $order
868
        ]);
869
    }
870
871
    protected function generateDeleteSql()
872
    {
873
        $options = $this->_queryOptions;
874
        if ($options) {
875
            $options = " {$options} ";
876
        }
877
878
        $limitOffset = $this->buildLimitOffset();
879
        return strtr('{delete}{options}{from}{where}{limit_offset}', [
880
            '{delete}' => 'DELETE',
881
            '{options}' => $options,
882
            '{from}' => $this->buildFrom(),
883
            '{where}' => $this->buildWhere(),
884
            '{limit_offset}' => $limitOffset,
885
        ]);
886
    }
887
888
    protected function generateInsertSql()
889
    {
890
        list($tableName, $values) = $this->_update;
891
        $this->setAlias();
892
        return $this->getAdapter()->sqlInsert($tableName, $values, $this->_queryOptions);
893
    }
894
895
    protected function generateUpdateSql()
896
    {
897
        list($tableName, $values) = $this->_update;
898
        $this->setAlias();
899
        return strtr('{update}{where}', [
900
            '{update}' => $this->getAdapter()->sqlUpdate($tableName, $values, $this->_queryOptions),
901
            '{where}' => $this->buildWhere(),
902
        ]);
903
    }
904
905
    /**
906
     * @return string
907
     * @throws QBException
908
     */
909
    public function toSQL()
910
    {
911
        switch ($this->getType())
912
        {
913
            case self::TYPE_SELECT:
914
                return $this->generateSelectSql();
915
916
            case self::TYPE_INSERT:
917
                return $this->generateInsertSql();
918
919
            case self::TYPE_UPDATE:
920
                return $this->generateUpdateSql();
921
922
            case self::TYPE_DELETE:
923
                return $this->generateDeleteSql();
924
        }
925
926
        throw new QBException('Unknown query type');
927
    }
928
929
    protected function buildHaving()
930
    {
931
        return $this->getAdapter()->sqlHaving(
932
            $this->parseCondition($this->_having)
933
        );
934
    }
935
936
    protected function buildLimitOffset()
937
    {
938
        return $this->getAdapter()->sqlLimitOffset(
939
            $this->_limit,
940
            $this->_offset
941
        );
942
    }
943
944
    protected function buildUnion()
945
    {
946
        $sql = '';
947
        foreach ($this->_union as  list($union, $all)) {
948
            $sql .= ' ' . $this->getAdapter()->sqlUnion($union, $all);
949
        }
950
951
        return empty($sql) ? '' : $sql;
952
    }
953
954
    /**
955
     * @param array|string|Q $having lookups
956
     * @return QueryBuilderInterface
957
     */
958
    public function setHaving($having)
959
    {
960
        $this->_having = [];
961
962
        return $this->addHaving($having);
963
    }
964
965
    public function addHaving($having)
966
    {
967
        if (!empty($having)) {
968
            $this->_having[] = $this->hydrate($having);
969
        }
970
971
        return $this;
972
    }
973
974
    public function setUnions(array $unions, $all = false)
975
    {
976
        $this->_union = [];
977
978
        if (!empty($unions)) {
979
            foreach ($unions as $union) {
980
                $this->addUnion($union, $all);
981
            }
982
        }
983
984
        return $this;
985
    }
986
987
    public function addUnion($union, $all = false)
988
    {
989
        $this->_union[] = [$union, $all];
990
        return $this;
991
    }
992
993
    /**
994
     * Makes alias for joined table
995
     * @param $table
996
     * @param bool $increment
997
     * @return string
998
     */
999
    public function makeAliasKey($table, $increment = false)
1000
    {
1001
        if ($increment) {
1002
            ++$this->_aliasesCount;
1003
        }
1004
1005
        $tableName = $this->getAdapter()->getRawTableName($table);
1006
1007
        if (strpos($tableName, '.') !== false) {
1008
            $tableName = substr($tableName, strpos($tableName, '.') + 1);
1009
        }
1010
1011
        return strtr('{table}_{count}', [
1012
            '{table}' => $tableName,
1013
            '{count}' => $this->_aliasesCount + 1
1014
        ]);
1015
    }
1016
1017
    /**
1018
     * @param string $table
1019
     * @param string $code
1020
     * @param string $topAlias
1021
     *
1022
     * @return string
1023
     */
1024
    public function makeMappedAliasKey($table, $code, $topAlias = null)
1025
    {
1026
        $key = $topAlias . '_' . $code;
1027
1028
        if (empty($this->_joinAlias[$table])) {
1029
            $this->_joinAlias[$table]['__alias_count__'] = 1;
1030
        }
1031
1032
        if (!empty($this->_joinAlias[$table][$key])) {
1033
            return $this->_joinAlias[$table][$key];
1034
        }
1035
1036
        $this->_joinAlias[$table][$key] = strtr('{table}_{count}', [
1037
            '{table}' => $this->getAdapter()->getRawTableName($table),
1038
            '{count}' => $this->_joinAlias[$table]['__alias_count__'] += 1
1039
        ]);
1040
1041
        return $this->_joinAlias[$table][$key];
1042
    }
1043
1044
    public function getJoin($tableName)
1045
    {
1046
        return $this->_join[$tableName];
1047
    }
1048
1049
    /**
1050
     * @param $column
1051
     * @return string
1052
     */
1053
    protected function addColumnAlias($column)
1054
    {
1055
        $tableAlias = $this->getAlias();
1056
        if ($tableAlias === null) {
1057
            return $column;
1058
        }
1059
1060
        if (strpos($column, '.') === false &&
1061
            strpos($column, '(') === false &&
1062
            strpos($column, 'SELECT') === false
1063
        ) {
1064
            return $tableAlias . '.' . $column;
1065
        }
1066
1067
        return $column;
1068
    }
1069
1070
    protected function hasAliasedField($column)
1071
    {
1072
        foreach ($this->_select as $alias => $item)
1073
        {
1074
            if (!is_numeric($alias) && $column === $alias) {
1075
                return true;
1076
            }
1077
        }
1078
1079
        return false;
1080
    }
1081
1082
    protected function applyTableAlias($column)
1083
    {
1084
        // If column already has alias - skip
1085
        if ((strpos($column, '.') === false) && !$this->hasAliasedField($column)) {
1086
            $tableAlias = $this->getAlias();
1087
            return $tableAlias === null ? $column : $tableAlias . '.' . $column;
1088
        }
1089
1090
        return $column;
1091
    }
1092
1093
    protected function buildJoin()
1094
    {
1095
        if (empty($this->_join)) {
1096
            return '';
1097
        }
1098
        $join = [];
1099
        foreach ($this->_join as $part) {
1100
            $join[] = $part;
1101
        }
1102
        return ' ' . implode(' ', $join);
1103
    }
1104
1105
1106
    /**
1107
     * @param $order
1108
     * @return array
1109
     */
1110
    protected function buildOrderJoin($order)
1111
    {
1112
        if (strpos($order, '-') === false) {
1113
            $direction = 'ASC';
1114
        }
1115
        else {
1116
            $direction = 'DESC';
1117
            $order = substr($order, 1);
1118
        }
1119
        $order = $this->getLookupBuilder()->fetchColumnName($order);
1120
        $newOrder = $this->getLookupBuilder()->buildJoin($this, $order);
1121
        if ($newOrder === false) {
1122
            return [$order, $direction];
1123
        }
1124
1125
        list($alias, $column) = $newOrder;
1126
        return [$alias . '.' . $column, $direction];
1127
    }
1128
1129
    public function getOrder()
1130
    {
1131
        return $this->_order;
1132
    }
1133
1134
    protected function buildOrder()
1135
    {
1136
        /**
1137
         * не делать проверку по empty(), проваливается половина тестов с ORDER BY
1138
         * и проваливается тест с построением JOIN по lookup
1139
         */
1140
        if ($this->_order === null) {
1141
            return '';
1142
        }
1143
1144
        $order = [];
1145
        if (is_array($this->_order)) {
1146
            foreach ($this->_order as $column) {
1147
                if ($column instanceof Expression) {
1148
                    $order[$column->toSQL()] = '';
1149
                }
1150
                else if ($column === '?') {
1151
                    $order[] = $this->getAdapter()->getRandomOrder();
1152
                }
1153
                else {
1154
                    list($newColumn, $direction) = $this->buildOrderJoin($column);
1155
                    $order[$this->applyTableAlias($newColumn)] = $direction;
1156
                }
1157
            }
1158
        }
1159
        else {
1160
            $order[] = $this->buildOrderJoin($this->_order);
1161
        }
1162
1163
        $sql = $this->getAdapter()->sqlOrderBy($order);
1164
        return empty($sql) ? '' : ' ORDER BY ' . $sql;
1165
    }
1166
1167
    /**
1168
     * @param $group
1169
     * @return string
1170
     */
1171
    protected function buildGroupJoin($group)
1172
    {
1173
        if (strpos($group, '.') === false) {
1174
            $newGroup = $this->getLookupBuilder()->fetchColumnName($group);
1175
            $newGroup = $this->getLookupBuilder()->buildJoin($this, $newGroup);
1176
1177
            if ($newGroup === false) {
1178
                return $group;
1179
            }
1180
1181
            list($alias, $column) = $newGroup;
1182
            return $alias . '.' . $column;
1183
        }
1184
1185
        return $group;
1186
    }
1187
1188
    protected function buildGroup()
1189
    {
1190
        $group = [];
1191
        if ($this->_group) {
1192
            foreach ($this->_group as $key => $column) {
1193
                $newColumn = $this->buildGroupJoin($column);
1194
                $group[] = $this->applyTableAlias($newColumn);
1195
            }
1196
        }
1197
1198
        $sql = $this->getAdapter()->sqlGroupBy($group);
1199
        return empty($sql) ? '' : ' GROUP BY ' . $sql;
1200
    }
1201
1202
    protected function buildFrom()
1203
    {
1204
        if ($this->_alias !== null && !is_array($this->_from)) {
1205
            $from = [$this->_alias => $this->_from];
1206
        }
1207
        else {
1208
            $from = $this->_from;
1209
        }
1210
        $sql = $this->getAdapter()->sqlFrom($from);
1211
        return empty($sql) ? '' : ' FROM ' . $sql;
1212
    }
1213
1214
    protected function hydrate($val)
1215
    {
1216
        if (is_object($val)) {
1217
            if ($val instanceof IToSql) {
1218
                $val->setQb($this);
1219
            }
1220
        }
1221
1222
        return $val;
1223
    }
1224
}
1225