Passed
Push — develop ( e333c2...b3554d )
by nguereza
02:25
created

Driver::getSetColumns()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
/**
4
 * Platine Database
5
 *
6
 * Platine Database is the abstraction layer using PDO with support of query and schema builder
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Database
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Driver.php
33
 *
34
 *  The Database Query Driver class
35
 *
36
 *  Each driver like MySQL, SQLite, Oracle, etc. need extend this class
37
 *
38
 *  @package    Platine\Database\Driver
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   http://www.iacademy.cf
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Database\Driver;
50
51
use DateTime;
52
use Platine\Database\Connection;
53
use Platine\Database\Query\Expression;
54
use Platine\Database\Query\QueryStatement;
55
use Platine\Database\Schema\AlterTable;
56
use Platine\Database\Schema\BaseColumn;
57
use Platine\Database\Schema\CreateTable;
58
use Platine\Database\Schema\ForeignKey;
59
60
61
/**
62
 * Class Driver
63
 *
64
 * @package Platine\Database\Driver
65
 */
66
class Driver
67
{
0 ignored issues
show
Coding Style introduced by
Opening brace must not be followed by a blank line
Loading history...
68
69
    /**
70
     * The driver default date format
71
     * @var string
72
     */
73
    protected string $dateFormat = 'Y-m-d H:i:s';
74
75
    /**
76
     * The quote identifier for a table and columns
77
     * @var string
78
     */
79
    protected string $identifier = '"%s"';
80
81
    /**
82
     * Each query separator
83
     * @var string
84
     */
85
    protected string $separator = ';';
86
87
    /**
88
     * The columns modifiers
89
     * @var array<string>
90
     */
91
    protected array $modifiers = [
92
        'unsigned',
93
        'nullable',
94
        'default',
95
        'autoincrement',
96
        'description'
97
    ];
98
99
    /**
100
     * Columns serial
101
     * @var array<string>
102
     */
103
    protected array $serials = [
104
        'tiny',
105
        'small',
106
        'normal',
107
        'medium',
108
        'big'
109
    ];
110
111
    /**
112
     * Auto increment value modifier
113
     * @var string
114
     */
115
    protected string $autoincrement = 'AUTO_INCREMENT';
116
117
    /**
118
     * The query parameters
119
     * @var array<mixed>
120
     */
121
    protected array $params = [];
122
123
    /**
124
     * The Connection instance
125
     * @var Connection
126
     */
127
    protected Connection $connection;
128
129
    /**
130
     * Class constructor
131
     * @param Connection $connection
132
     */
133
    public function __construct(Connection $connection)
134
    {
135
        $this->connection = $connection;
136
    }
137
138
    /**
139
     * Returns the SQL for SELECT statement
140
     * @param QueryStatement $select
141
     *
142
     * @return string
143
     */
144
    public function select(QueryStatement $select): string
145
    {
146
        $sql = $select->hasDistinct() ? 'SELECT DISTINCT ' : 'SELECT ';
147
        $sql .= $this->getColumnList($select->getColumns());
148
        $sql .= $this->getInto($select->getIntoTable());
149
        $sql .= ' FROM ';
150
        $sql .= $this->getTableList($select->getTables());
151
        $sql .= $this->getJoins($select->getJoins());
152
        $sql .= $this->getWheres($select->getWheres());
153
        $sql .= $this->getGroupBy($select->getGroupBy());
154
        $sql .= $this->getHaving($select->getHaving());
155
        $sql .= $this->getOrders($select->getOrder());
156
        $sql .= $this->getLimit($select->getLimit());
157
        $sql .= $this->getOffset($select->getOffset());
158
159
        return $sql;
160
    }
161
162
    /**
163
     * Return SQL for INSERT statement
164
     * @param QueryStatement $insert
165
     *
166
     * @return string
167
     */
168
    public function insert(QueryStatement $insert): string
169
    {
170
        $columns = $this->getColumnList($insert->getColumns());
171
172
        $sql = 'INSERT INTO ';
173
        $sql .= $this->getTableList($insert->getTables());
174
        $sql .= ($columns == '*') ? '' : '(' . $columns . ')';
175
        $sql .= $this->getInsertValues($insert->getValues());
176
177
        return $sql;
178
    }
179
180
    /**
181
     * Return the SQL for UPDATE statement
182
     * @param QueryStatement $update
183
     *
184
     * @return string
185
     */
186
    public function update(QueryStatement $update): string
187
    {
188
        $sql = 'UPDATE ';
189
        $sql .= $this->getTableList($update->getTables());
190
        $sql .= $this->getJoins($update->getJoins());
191
        $sql .= $this->getSetColumns($update->getColumns());
192
        $sql .= $this->getWheres($update->getWheres());
193
194
        return $sql;
195
    }
196
197
    /**
198
     * Return the SQL for DELETE statement
199
     * @param QueryStatement $delete
200
     *
201
     * @return string
202
     */
203
    public function delete(QueryStatement $delete): string
204
    {
205
        $sql = 'DELETE ' . $this->getTableList($delete->getTables());
206
        $sql .= $sql === 'DELETE ' ? 'FROM ' : ' FROM ';
207
        $sql .= $this->getTableList($delete->getFrom());
208
        $sql .= $this->getJoins($delete->getJoins());
209
        $sql .= $this->getWheres($delete->getWheres());
210
211
        return $sql;
212
    }
213
214
    /**
215
     * Return the date format
216
     * @return string
217
     */
218
    public function getDateFormat(): string
219
    {
220
        return $this->dateFormat;
221
    }
222
223
    /**
224
     *
225
     * @param string $format
226
     * @return $this
227
     */
228
    public function setDateFormat(string $format): self
229
    {
230
        $this->dateFormat = $format;
231
232
        return $this;
233
    }
234
235
    /**
236
     * Set the drive options
237
     * @param array<string, mixed> $options
238
     */
239
    public function setOptions(array $options): void
240
    {
241
        foreach ($options as $name => $value) {
242
            $this->{$name} = $value;
243
        }
244
    }
245
246
    /**
247
     * @param array<mixed> $params
248
     *
249
     * @return string
250
     */
251
    public function params(array $params): string
252
    {
253
        return implode(', ', array_map([$this, 'param'], $params));
254
    }
255
256
    /**
257
     * @return array<mixed>
258
     */
259
    public function getParams(): array
260
    {
261
        $params = $this->params;
262
        $this->params = [];
263
264
        return $params;
265
    }
266
267
    /**
268
     * @param Expression[]|string[] $columns
269
     *
270
     * @return string
271
     */
272
    public function columns(array $columns): string
273
    {
274
        return implode(', ', array_map([$this, 'quoteIdentifier'], $columns));
275
    }
276
277
    /**
278
     * @param string $value
279
     *
280
     * @return string
281
     */
282
    public function quote(string $value): string
283
    {
284
        return "'" . str_replace("'", "''", $value) . "'";
285
    }
286
287
    /**
288
     * Return the SQL for the current database
289
     * @return array<string, mixed>
290
     */
291
    public function getDatabaseName(): array
292
    {
293
        return [
294
            'sql' => 'SELECT database()',
295
            'params' => []
296
        ];
297
    }
298
299
    /**
300
     *
301
     * @param string $current
302
     * @param string $new
303
     * @return array<string, mixed>
304
     */
305
    public function renameTable(string $current, string $new): array
306
    {
307
        return [
308
            'sql' => 'RENAME TABLE ' . $this->quoteIdentifier($current)
309
            . ' TO ' . $this->quoteIdentifier($new),
310
            'params' => []
311
        ];
312
    }
313
314
    /**
315
     *
316
     * @param string $database
317
     * @return array<string, mixed>
318
     */
319
    public function getTables(string $database): array
320
    {
321
        $sql = sprintf(
322
            'SELECT %s FROM %s.%s WHERE table_type = ? '
323
                . 'AND table_schema = ? ORDER BY %s ASC',
324
            $this->quoteIdentifier('table_name'),
325
            $this->quoteIdentifier('information_schema'),
326
            $this->quoteIdentifier('tables'),
327
            $this->quoteIdentifier('table_name'),
328
        );
329
330
        return [
331
            'sql' => $sql,
332
            'params' => ['BASE TABLE', $database]
333
        ];
334
    }
335
336
    /**
337
     *
338
     * @param string $database
339
     * @param string $table
340
     * @return array<string, mixed>
341
     */
342
    public function getColumns(string $database, string $table): array
343
    {
344
        $sql = sprintf(
345
            'SELECT %s AS %s, %s AS %s FROM %s.%s WHERE %s = ? '
346
                . 'AND %s = ? ORDER BY %s ASC',
347
            $this->quoteIdentifier('column_name'),
348
            $this->quoteIdentifier('name'),
349
            $this->quoteIdentifier('column_type'),
350
            $this->quoteIdentifier('type'),
351
            $this->quoteIdentifier('information_schema'),
352
            $this->quoteIdentifier('columns'),
353
            $this->quoteIdentifier('table_schema'),
354
            $this->quoteIdentifier('table_name'),
355
            $this->quoteIdentifier('ordinal_position'),
356
        );
357
358
        return [
359
            'sql' => $sql,
360
            'params' => [$database, $table]
361
        ];
362
    }
363
364
    /**
365
     *
366
     * @param CreateTable $schema
367
     * @return array<int, array<string, mixed>>
368
     */
369
    public function create(CreateTable $schema): array
370
    {
371
        $sql = 'CREATE TABLE ' . $this->quoteIdentifier($schema->getTableName());
372
        $sql .= "(\n";
373
        $sql .= $this->getSchemaColumns($schema->getColumns());
374
        $sql .= $this->getPrimaryKey($schema);
375
        $sql .= $this->getUniqueKeys($schema);
376
        $sql .= $this->getForeignKeys($schema);
377
        $sql .= ")\n";
378
        $sql .= $this->getEngine($schema);
379
380
        $commands = [];
381
382
        $commands[] = [
383
            'sql' => $sql,
384
            'params' => []
385
        ];
386
387
        foreach ($this->getIndexKeys($schema) as $index) {
388
            $commands[] = [
389
                'sql' => $index,
390
                'params' => []
391
            ];
392
        }
393
394
        return $commands;
395
    }
396
397
    /**
398
     *
399
     * @param AlterTable $schema
400
     * @return array<int, array<string, mixed>>
401
     */
402
    public function alter(AlterTable $schema): array
403
    {
404
        $commands = [];
405
406
        foreach ($schema->getCommands() as $command) {
407
            $callback = 'get' . ucfirst($command['type']);
408
            $sql = $this->{$callback}($schema, $command['data']);
409
410
            if ($sql === '') {
411
                continue;
412
            }
413
414
            $commands[] = [
415
                'sql' => $sql,
416
                'params' => $this->getParams()
417
            ];
418
        }
419
420
        return $commands;
421
    }
422
423
    /**
424
     *
425
     * @param string $table
426
     * @return array<string, mixed>
427
     */
428
    public function drop(string $table): array
429
    {
430
        return [
431
            'sql' => 'DROP TABLE ' . $this->quoteIdentifier($table),
432
            'params' => []
433
        ];
434
    }
435
436
    /**
437
     *
438
     * @param string $table
439
     * @return array<string, mixed>
440
     */
441
    public function truncate(string $table): array
442
    {
443
        return [
444
            'sql' => 'TRUNCATE TABLE ' . $this->quoteIdentifier($table),
445
            'params' => []
446
        ];
447
    }
448
449
    /**
450
     * @param mixed $value
451
     *
452
     * @return string
453
     */
454
    protected function param($value): string
455
    {
456
        if ($value instanceof Expression) {
457
            return $this->getExpressions($value->getExpressions());
458
        } elseif ($value instanceof DateTime) {
459
            $this->params[] = $value->format($this->dateFormat);
460
        } else {
461
            $this->params[] = $value;
462
        }
463
464
        return '?';
465
    }
466
467
    /**
468
     * Get the value by convert it to the type
469
     * @param mixed $value
470
     * @return mixed
471
     *
472
     */
473
    protected function value($value)
474
    {
475
        if (is_numeric($value)) {
476
            return $value;
477
        }
478
479
        if (is_bool($value)) {
480
            return $value ? 1 : 0;
481
        }
482
483
        if (is_string($value)) {
484
            return "'" . str_replace("'", "''", $value) . "'";
485
        }
486
487
        return 'NULL';
488
    }
489
490
    /**
491
     * Add quote identifier like "", ``
492
     * @param mixed $value
493
     *
494
     * @return string
495
     */
496
    protected function quoteIdentifier($value): string
497
    {
498
        if ($value instanceof Expression) {
499
            return $this->getExpressions($value->getExpressions());
500
        }
501
502
        $identifiers = [];
503
504
        foreach (explode('.', $value) as $segment) {
505
            if ($segment === '*') {
506
                $identifiers[] = $segment;
507
            } else {
508
                $identifiers[] = sprintf($this->identifier, $segment);
509
            }
510
        }
511
512
        return implode('.', $identifiers);
513
    }
514
515
    /**
516
     *
517
     * @param array<mixed> $values
518
     * @param string $separator
519
     * @return string
520
     */
521
    protected function quoteIdentifiers(array $values, string $separator = ', '): string
522
    {
523
        return implode($separator, array_map([$this, 'quoteIdentifier'], $values));
524
    }
525
526
    /**
527
     * Handle expressions
528
     * @param array<int, array<string, mixed>> $expressions
529
     *
530
     * @return string
531
     */
532
    protected function getExpressions(array $expressions): string
533
    {
534
        $sql = [];
535
536
        foreach ($expressions as $expression) {
537
            switch ($expression['type']) {
538
                case 'column':
539
                    $sql[] = $this->quoteIdentifier($expression['value']);
540
                    break;
541
                case 'op':
542
                    $sql[] = $expression['value'];
543
                    break;
544
                case 'value':
545
                    $sql[] = $this->param($expression['value']);
546
                    break;
547
                case 'group':
548
                    $expr = $expression['value'];
549
                    $sql[] = '(' . $this->getExpressions($expr->getExpressions()) . ')';
550
                    break;
551
                case 'function':
552
                    $sql[] = $this->getSqlFunction($expression['value']);
553
                    break;
554
                case 'subquery':
555
                    $subQuery = $expression['value'];
556
                    $sql[] = '(' . $this->select($subQuery->getQueryStatement()) . ')';
557
                    break;
558
            }
559
        }
560
561
        return implode(' ', $sql);
562
    }
563
564
    /**
565
     * Handle SQL function
566
     * @param array<string, mixed> $functions
567
     *
568
     * @return string
569
     */
570
    protected function getSqlFunction(array $functions): string
571
    {
572
        $method = $functions['type'] . $functions['name'];
573
574
        return $this->{$method}($functions);
575
    }
576
577
    /**
578
     * Handle columns
579
     * @param array<int, array<string, mixed>> $columns
580
     *
581
     * @return string
582
     */
583
    protected function getColumnList(array $columns): string
584
    {
585
        if (empty($columns)) {
586
            return '*';
587
        }
588
        $sql = [];
589
590
        foreach ($columns as $column) {
591
            if (isset($column['alias'])) {
592
                $sql[] = $this->quoteIdentifier($column['name'])
593
                        . ' AS ' . $this->quoteIdentifier($column['alias']);
594
            } else {
595
                $sql[] = $this->quoteIdentifier($column['name']);
596
            }
597
        }
598
599
        return implode(', ', $sql);
600
    }
601
602
    /**
603
     * Handle schema columns
604
     * @param array<int|string, BaseColumn> $columns list of BaseColumn
605
     * @return string
606
     */
607
    protected function getSchemaColumns(array $columns): string
608
    {
609
        $sql = [];
610
611
        foreach ($columns as $column) {
612
            $line = $this->quoteIdentifier($column->getName());
613
            $line .= $this->getColumnType($column);
614
            $line .= $this->getColumnModifiers($column);
615
616
            $sql[] = $line;
617
        }
618
619
        return implode(",\n", $sql);
620
    }
621
622
    /**
623
     *
624
     * @param BaseColumn $column
625
     * @return string
626
     */
627
    protected function getColumnType(BaseColumn $column): string
628
    {
629
        $type = $column->getType();
630
        $result = '';
631
        if (is_string($type)) {
632
            $callback = 'getType' . ucfirst($type);
633
            $result = trim($this->{$callback}($column));
634
635
            if ($result !== '') {
636
                $result = ' ' . $result;
637
            }
638
        }
639
640
        return $result;
641
    }
642
643
    /**
644
     *
645
     * @param BaseColumn $column
646
     * @return string
647
     */
648
    protected function getColumnModifiers(BaseColumn $column): string
649
    {
650
        $line = '';
651
        foreach ($this->modifiers as $modifier) {
652
            $callback = 'getModifier' . ucfirst($modifier);
653
            $result = trim($this->{$callback}($column));
654
655
656
            if ($result !== '') {
657
                $result = ' ' . $result;
658
            }
659
660
            $line .= $result;
661
        }
662
663
        return $line;
664
    }
665
666
    /**
667
     *
668
     * @param BaseColumn $column
669
     * @return string
670
     */
671
    protected function getTypeInteger(BaseColumn $column): string
672
    {
673
        return 'INT';
674
    }
675
676
    /**
677
     *
678
     * @param BaseColumn $column
679
     * @return string
680
     */
681
    protected function getTypeFloat(BaseColumn $column): string
682
    {
683
        return 'FLOAT';
684
    }
685
686
    /**
687
     *
688
     * @param BaseColumn $column
689
     * @return string
690
     */
691
    protected function getTypeDouble(BaseColumn $column): string
692
    {
693
        return 'DOUBLE';
694
    }
695
696
    /**
697
     *
698
     * @param BaseColumn $column
699
     * @return string
700
     */
701
    protected function getTypeDecimal(BaseColumn $column): string
702
    {
703
        return 'DECIMAL';
704
    }
705
    
706
    /**
707
     *
708
     * @param BaseColumn $column
709
     * @return string
710
     */
711
    protected function getTypeEnum(BaseColumn $column): string
712
    {
713
        return 'ENUM';
714
    }
715
716
    /**
717
     *
718
     * @param BaseColumn $column
719
     * @return string
720
     */
721
    protected function getTypeBoolean(BaseColumn $column): string
722
    {
723
        return 'BOOLEAN';
724
    }
725
726
    /**
727
     *
728
     * @param BaseColumn $column
729
     * @return string
730
     */
731
    protected function getTypeBinary(BaseColumn $column): string
732
    {
733
        return 'BLOB';
734
    }
735
736
    /**
737
     *
738
     * @param BaseColumn $column
739
     * @return string
740
     */
741
    protected function getTypeText(BaseColumn $column): string
742
    {
743
        return 'TEXT';
744
    }
745
746
    /**
747
     *
748
     * @param BaseColumn $column
749
     * @return string
750
     */
751
    protected function getTypeString(BaseColumn $column): string
752
    {
753
        return 'VARCHAR(' . $this->value($column->get('length', 255)) . ')';
754
    }
755
756
    /**
757
     *
758
     * @param BaseColumn $column
759
     * @return string
760
     */
761
    protected function getTypeFixed(BaseColumn $column): string
762
    {
763
        return 'CHAR(' . $this->value($column->get('length', 255)) . ')';
764
    }
765
766
    /**
767
     *
768
     * @param BaseColumn $column
769
     * @return string
770
     */
771
    protected function getTypeTime(BaseColumn $column): string
772
    {
773
        return 'TIME';
774
    }
775
776
    /**
777
     *
778
     * @param BaseColumn $column
779
     * @return string
780
     */
781
    protected function getTypeTimestamp(BaseColumn $column): string
782
    {
783
        return 'TIMESTAMP';
784
    }
785
786
    /**
787
     *
788
     * @param BaseColumn $column
789
     * @return string
790
     */
791
    protected function getTypeDate(BaseColumn $column): string
792
    {
793
        return 'DATE';
794
    }
795
796
    /**
797
     *
798
     * @param BaseColumn $column
799
     * @return string
800
     */
801
    protected function getTypeDatetime(BaseColumn $column): string
802
    {
803
        return 'DATETIME';
804
    }
805
806
    /**
807
     *
808
     * @param BaseColumn $column
809
     * @return string
810
     */
811
    protected function getModifierUnsigned(BaseColumn $column): string
812
    {
813
        return $column->get('unsigned', false) ? 'UNSIGNED' : '';
814
    }
815
816
    /**
817
     *
818
     * @param BaseColumn $column
819
     * @return string
820
     */
821
    protected function getModifierNullable(BaseColumn $column): string
822
    {
823
        return $column->get('nullable', true) ? '' : 'NOT NULL';
824
    }
825
826
    /**
827
     *
828
     * @param BaseColumn $column
829
     * @return string
830
     */
831
    protected function getModifierDefault(BaseColumn $column): string
832
    {
833
        return $column->get('default', null) === null ? '' : 'DEFAULT ' . $this->value($column->get('default'));
834
    }
835
836
    /**
837
     *
838
     * @param BaseColumn $column
839
     * @return string
840
     */
841
    protected function getModifierDescription(BaseColumn $column): string
842
    {
843
        return $column->get('description', null) === null ? '' : 'COMMENT ' . $this->value($column->get('description'));
844
    }
845
846
    /**
847
     *
848
     * @param BaseColumn $column
849
     * @return string
850
     */
851
    protected function getModifierAutoincrement(BaseColumn $column): string
852
    {
853
        if (
854
            $column->getType() !== 'integer'
855
            || !in_array($column->get('size', 'normal'), $this->serials)
856
        ) {
857
            return '';
858
        }
859
        return $column->get('autoincrement', false) ? $this->autoincrement : '';
860
    }
861
862
    /**
863
     *
864
     * @param CreateTable $schema
865
     * @return string
866
     */
867
    protected function getPrimaryKey(CreateTable $schema): string
868
    {
869
        $primaryKey = $schema->getPrimaryKey();
870
        if (empty($primaryKey)) {
871
            return '';
872
        }
873
874
        return ",\n" . 'CONSTRAINT ' . $this->quoteIdentifier($primaryKey['name'])
875
                . ' PRIMARY KEY (' . $this->quoteIdentifiers($primaryKey['columns']) . ')';
876
    }
877
878
    /**
879
     *
880
     * @param CreateTable $schema
881
     * @return string
882
     */
883
    protected function getUniqueKeys(CreateTable $schema): string
884
    {
885
        $indexes = $schema->getUniqueKeys();
886
887
        if (empty($indexes)) {
888
            return '';
889
        }
890
891
        $sql = [];
892
893
        foreach ($indexes as $name => $columns) {
894
            $sql[] = 'CONSTRAINT ' . $this->quoteIdentifier($name)
895
                    . ' UNIQUE (' . $this->quoteIdentifiers($columns) . ')';
896
        }
897
898
        return ",\n" . implode(",\n", $sql);
899
    }
900
901
    /**
902
     *
903
     * @param CreateTable $schema
904
     * @return array<int, string>
905
     */
906
    protected function getIndexKeys(CreateTable $schema): array
907
    {
908
        $indexes = $schema->getIndexes();
909
910
        if (empty($indexes)) {
911
            return [];
912
        }
913
914
        $sql = [];
915
        $table = $this->quoteIdentifier($schema->getTableName());
916
917
        foreach ($indexes as $name => $columns) {
918
            $sql[] = 'CREATE INDEX ' . $this->quoteIdentifier($name)
919
                    . ' ON ' . $table . '(' . $this->quoteIdentifiers($columns) . ')';
920
        }
921
922
        return $sql;
923
    }
924
925
    /**
926
     *
927
     * @param CreateTable $schema
928
     * @return string
929
     */
930
    protected function getForeignKeys(CreateTable $schema): string
931
    {
932
        $keys = $schema->getForeignKeys();
933
934
        if (empty($keys)) {
935
            return '';
936
        }
937
938
        $sql = [];
939
940
        foreach ($keys as $name => $key) {
941
            $cmd = 'CONSTRAINT ' . $this->quoteIdentifier($name)
942
                    . ' FOREIGN KEY (' . $this->quoteIdentifiers($key->getColumns()) . ') ';
943
            $cmd .= 'REFERENCES ' . $this->quoteIdentifier($key->getReferenceTable())
944
                    . ' (' . $this->quoteIdentifiers($key->getReferenceColumns()) . ')';
945
946
            foreach ($key->getActions() as $actionName => $action) {
947
                $cmd .= ' ' . $actionName . ' ' . $action;
948
            }
949
            $sql[] = $cmd;
950
        }
951
952
        return ",\n" . implode(",\n", $sql);
953
    }
954
955
    /**
956
     *
957
     * @param CreateTable $schema
958
     * @return string
959
     */
960
    protected function getEngine(CreateTable $schema): string
961
    {
962
        $engine = $schema->getEngine();
963
        if ($engine === null) {
964
            return '';
965
        }
966
967
        return ' ENGINE = ' . strtoupper($engine);
968
    }
969
970
    /**
971
     *
972
     * @param AlterTable $schema
973
     * @param mixed $data
974
     * @return string
975
     */
976
    protected function getDropPrimaryKey(AlterTable $schema, $data): string
977
    {
978
        return sprintf(
979
            'ALTER TABLE %s DROP CONSTRAINT %s',
980
            $this->quoteIdentifier($schema->getTableName()),
981
            $this->quoteIdentifier($data)
982
        );
983
    }
984
985
    /**
986
     *
987
     * @param AlterTable $schema
988
     * @param mixed $data
989
     * @return string
990
     */
991
    protected function getDropUniqueKey(AlterTable $schema, $data): string
992
    {
993
        return sprintf(
994
            'ALTER TABLE %s DROP CONSTRAINT %s',
995
            $this->quoteIdentifier($schema->getTableName()),
996
            $this->quoteIdentifier($data)
997
        );
998
    }
999
1000
    /**
1001
     *
1002
     * @param AlterTable $schema
1003
     * @param mixed $data
1004
     * @return string
1005
     */
1006
    protected function getDropIndex(AlterTable $schema, $data): string
1007
    {
1008
        return sprintf(
1009
            'DROP INDEX %s.%s',
1010
            $this->quoteIdentifier($schema->getTableName()),
1011
            $this->quoteIdentifier($data)
1012
        );
1013
    }
1014
1015
    /**
1016
     *
1017
     * @param AlterTable $schema
1018
     * @param mixed $data
1019
     * @return string
1020
     */
1021
    protected function getDropForeignKey(AlterTable $schema, $data): string
1022
    {
1023
        return sprintf(
1024
            'ALTER TABLE %s DROP CONSTRAINT %s',
1025
            $this->quoteIdentifier($schema->getTableName()),
1026
            $this->quoteIdentifier($data)
1027
        );
1028
    }
1029
1030
    /**
1031
     *
1032
     * @param AlterTable $schema
1033
     * @param mixed $data
1034
     * @return string
1035
     */
1036
    protected function getDropColumn(AlterTable $schema, $data): string
1037
    {
1038
        return sprintf(
1039
            'ALTER TABLE %s DROP COLUMN %s',
1040
            $this->quoteIdentifier($schema->getTableName()),
1041
            $this->quoteIdentifier($data)
1042
        );
1043
    }
1044
1045
    /**
1046
     *
1047
     * @param AlterTable $schema
1048
     * @param mixed $data
1049
     * @return string
1050
     */
1051
    protected function getRenameColumn(AlterTable $schema, $data): string
1052
    {
1053
        //TODO: please implement it in subclass
1054
        return '';
1055
    }
1056
1057
    /**
1058
     *
1059
     * @param AlterTable $schema
1060
     * @param mixed $data
1061
     * @return string
1062
     */
1063
    protected function getModifyColumn(AlterTable $schema, $data): string
1064
    {
1065
        return sprintf(
1066
            'ALTER TABLE %s MODIFY COLUMN %s',
1067
            $this->quoteIdentifier($schema->getTableName()),
1068
            $this->getSchemaColumns([$data])
1069
        );
1070
    }
1071
1072
    /**
1073
     *
1074
     * @param AlterTable $schema
1075
     * @param mixed $data
1076
     * @return string
1077
     */
1078
    protected function getAddColumn(AlterTable $schema, $data): string
1079
    {
1080
        return sprintf(
1081
            'ALTER TABLE %s ADD COLUMN %s',
1082
            $this->quoteIdentifier($schema->getTableName()),
1083
            $this->getSchemaColumns([$data])
1084
        );
1085
    }
1086
1087
    /**
1088
     *
1089
     * @param AlterTable $schema
1090
     * @param mixed $data
1091
     * @return string
1092
     */
1093
    protected function getAddPrimary(AlterTable $schema, $data): string
1094
    {
1095
        return sprintf(
1096
            'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)',
1097
            $this->quoteIdentifier($schema->getTableName()),
1098
            $this->quoteIdentifier($data['name']),
1099
            $this->quoteIdentifiers($data['columns'])
1100
        );
1101
    }
1102
1103
    /**
1104
     *
1105
     * @param AlterTable $schema
1106
     * @param mixed $data
1107
     * @return string
1108
     */
1109
    protected function getAddUnique(AlterTable $schema, $data): string
1110
    {
1111
        return sprintf(
1112
            'ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)',
1113
            $this->quoteIdentifier($schema->getTableName()),
1114
            $this->quoteIdentifier($data['name']),
1115
            $this->quoteIdentifiers($data['columns'])
1116
        );
1117
    }
1118
1119
    /**
1120
     *
1121
     * @param AlterTable $schema
1122
     * @param mixed $data
1123
     * @return string
1124
     */
1125
    protected function getAddIndex(AlterTable $schema, $data): string
1126
    {
1127
        return sprintf(
1128
            'CREATE INDEX %s ON %s (%s)',
1129
            $this->quoteIdentifier($data['name']),
1130
            $this->quoteIdentifier($schema->getTableName()),
1131
            $this->quoteIdentifiers($data['columns'])
1132
        );
1133
    }
1134
1135
    /**
1136
     *
1137
     * @param AlterTable $schema
1138
     * @param mixed $data
1139
     * @return string
1140
     */
1141
    protected function getAddForeign(AlterTable $schema, $data): string
1142
    {
1143
        /** @var ForeignKey $key */
1144
        $key = $data['foreign'];
1145
        return sprintf(
1146
            'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)',
1147
            $this->quoteIdentifier($schema->getTableName()),
1148
            $this->quoteIdentifier($data['name']),
1149
            $this->quoteIdentifiers($key->getColumns()),
1150
            $this->quoteIdentifier($key->getReferenceTable()),
1151
            $this->quoteIdentifiers($key->getReferenceColumns()),
1152
        );
1153
    }
1154
1155
    /**
1156
     *
1157
     * @param AlterTable $schema
1158
     * @param mixed $data
1159
     * @return string
1160
     */
1161
    protected function getSetDefaultValue(AlterTable $schema, $data): string
1162
    {
1163
        return sprintf(
1164
            'ALTER TABLE %s ALTER COLUMN %s SET DEFAULT (%s)',
1165
            $this->quoteIdentifier($schema->getTableName()),
1166
            $this->quoteIdentifier($data['column']),
1167
            $this->value($data['value'])
1168
        );
1169
    }
1170
1171
    /**
1172
     *
1173
     * @param AlterTable $schema
1174
     * @param mixed $data
1175
     * @return string
1176
     */
1177
    protected function getDropDefaultValue(AlterTable $schema, $data): string
1178
    {
1179
        return sprintf(
1180
            'ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
1181
            $this->quoteIdentifier($schema->getTableName()),
1182
            $this->quoteIdentifier($data)
1183
        );
1184
    }
1185
1186
    /**
1187
     * Handle into the table
1188
     *
1189
     * @param string|null $table
1190
     *
1191
     * @return string
1192
     */
1193
    protected function getInto(?string $table): string
1194
    {
1195
        if ($table === null) {
1196
            return '';
1197
        }
1198
        return ' INTO ' . $this->quoteIdentifier($table);
1199
    }
1200
1201
    /**
1202
     * Handle tables
1203
     * @param array<mixed, string> $tables
1204
     *
1205
     * @return string
1206
     */
1207
    protected function getTableList(array $tables): string
1208
    {
1209
        if (empty($tables)) {
1210
            return '';
1211
        }
1212
        $sql = [];
1213
        foreach ($tables as $name => $alias) {
1214
            if (is_string($name)) {
1215
                $sql[] = $this->quoteIdentifier($name) . ' AS ' . $this->quoteIdentifier($alias);
1216
            } else {
1217
                $sql[] = $this->quoteIdentifier($alias);
1218
            }
1219
        }
1220
1221
        return implode(', ', $sql);
1222
    }
1223
1224
    /**
1225
     * Handle for joins
1226
     * @param array<int, mixed> $joins
1227
     *
1228
     * @return string
1229
     */
1230
    protected function getJoins(array $joins): string
1231
    {
1232
        if (empty($joins)) {
1233
            return '';
1234
        }
1235
        $sql = [];
1236
1237
        foreach ($joins as $join) {
1238
            $joinObject = $join['join'];
1239
1240
            $on = '';
1241
            if ($joinObject) {
1242
                $on = $this->getJoinConditions($joinObject->getJoinConditions());
1243
            }
1244
1245
            if ($on !== '') {
1246
                $on = ' ON ' . $on;
1247
            }
1248
1249
            $sql[] = $join['type'] . ' JOIN ' . $this->getTableList($join['table']) . $on;
1250
        }
1251
1252
        return ' ' . implode(' ', $sql);
1253
    }
1254
1255
    /**
1256
     * Handle for the join conditions
1257
     * @param array<int, mixed> $conditions
1258
     * @return string
1259
     */
1260
    protected function getJoinConditions(array $conditions): string
1261
    {
1262
        if (empty($conditions)) {
1263
            return '';
1264
        }
1265
1266
        $sql = [];
1267
1268
        $sql[] = $this->{$conditions[0]['type']}($conditions[0]);
1269
1270
        $count = count($conditions);
1271
        for ($i = 1; $i < $count; $i++) {
1272
            $sql[] = $conditions[$i]['separator'] . ' ' . $this->{$conditions[$i]['type']}($conditions[$i]);
1273
        }
1274
1275
        return implode(' ', $sql);
1276
    }
1277
1278
    /**
1279
     * Handle group by
1280
     * @param Expression[]|string[] $groupBy
1281
     *
1282
     * @return string
1283
     */
1284
    protected function getGroupBy(array $groupBy): string
1285
    {
1286
        return empty($groupBy) ? '' : ' GROUP BY ' . $this->columns($groupBy);
1287
    }
1288
1289
    /**
1290
     * Handle for Order
1291
     * @param array<int, array<string, mixed>> $orders
1292
     * @return string
1293
     */
1294
    protected function getOrders(array $orders): string
1295
    {
1296
        if (empty($orders)) {
1297
            return '';
1298
        }
1299
        $sql = [];
1300
        foreach ($orders as $order) {
1301
            $sql[] = $this->columns($order['columns']) . ' ' . $order['order'];
1302
        }
1303
1304
        return ' ORDER BY ' . implode(', ', $sql);
1305
    }
1306
1307
    /**
1308
     * Handle columns for set (UPDATE)
1309
     * @param array<int, array<string, mixed>> $columns
1310
     * @return string
1311
     */
1312
    protected function getSetColumns(array $columns): string
1313
    {
1314
        if (empty($columns)) {
1315
            return '';
1316
        }
1317
        $sql = [];
1318
1319
        foreach ($columns as $column) {
1320
            $sql[] = $this->quoteIdentifier($column['column']) . ' = ' . $this->param($column['value']);
1321
        }
1322
1323
        return ' SET ' . implode(', ', $sql);
1324
    }
1325
1326
    /**
1327
     * Handler where
1328
     * @param array<int, mixed> $wheres
1329
     * @param bool $prefix
1330
     *
1331
     * @return string
1332
     */
1333
    protected function getWheres(array $wheres, bool $prefix = true): string
1334
    {
1335
        $sql = $this->getWheresHaving($wheres);
1336
        if (empty($sql)) {
1337
            return '';
1338
        }
1339
        return ($prefix ? ' WHERE ' : '') . $sql;
1340
    }
1341
1342
    /**
1343
     * Handle for having
1344
     * @param array<int, mixed> $having
1345
     * @param bool $prefix
1346
     * @return string
1347
     */
1348
    protected function getHaving(array $having, bool $prefix = true): string
1349
    {
1350
        $sql = $this->getWheresHaving($having);
1351
        if (empty($sql)) {
1352
            return '';
1353
        }
1354
        return ($prefix ? ' HAVING ' : '') . $sql;
1355
    }
1356
1357
    /**
1358
     * Return the build part for where or having
1359
     * @param array<int, mixed> $values
1360
     *
1361
     * @return string
1362
     */
1363
    protected function getWheresHaving(array $values): string
1364
    {
1365
        if (empty($values)) {
1366
            return '';
1367
        }
1368
        $sql = [];
1369
        $sql[] = $this->{$values[0]['type']}($values[0]);
1370
        $count = count($values);
1371
1372
        for ($i = 1; $i < $count; $i++) {
1373
            $sql[] = $values[$i]['separator'] . ' ' . $this->{$values[$i]['type']}($values[$i]);
1374
        }
1375
        return implode(' ', $sql);
1376
    }
1377
1378
    /**
1379
     * Handle for insert values
1380
     * @param array<int, mixed> $values
1381
     * @return string
1382
     */
1383
    protected function getInsertValues(array $values): string
1384
    {
1385
        return ' VALUES (' . $this->params($values) . ')';
1386
    }
1387
1388
    /**
1389
     * Handle for limit
1390
     * @param int $limit
1391
     * @return string
1392
     */
1393
    protected function getLimit(int $limit): string
1394
    {
1395
        return ($limit === 0) ? '' : ' LIMIT ' . $this->param($limit);
1396
    }
1397
1398
    /**
1399
     * Handle for offset
1400
     * @param int $offset
1401
     * @return string
1402
     */
1403
    protected function getOffset(int $offset): string
1404
    {
1405
        return ($offset < 0) ? '' : ' OFFSET ' . $this->param($offset);
1406
    }
1407
1408
    /**
1409
     * @param array<string, mixed> $join
1410
     * @return string
1411
     */
1412
    protected function joinColumn(array $join): string
1413
    {
1414
        return sprintf(
1415
            '%s %s %s',
1416
            $this->quoteIdentifier($join['column1']),
1417
            $join['operator'],
1418
            $this->quoteIdentifier($join['column2'])
1419
        );
1420
    }
1421
1422
    /**
1423
     * @param array<string, mixed> $join
1424
     * @return string
1425
     */
1426
    protected function joinNested(array $join): string
1427
    {
1428
        return '(' . $this->getJoinConditions($join['join']->getJoinConditions()) . ')';
1429
    }
1430
1431
    /**
1432
     * @param array<string, mixed> $join
1433
     * @return string
1434
     */
1435
    protected function joinExpression(array $join): string
1436
    {
1437
        return $this->quoteIdentifier($join['expression']);
1438
    }
1439
1440
    /**
1441
     * @param array<string, mixed> $where
1442
     * @return string
1443
     */
1444
    protected function whereColumn(array $where): string
1445
    {
1446
        return sprintf(
1447
            '%s %s %s',
1448
            $this->quoteIdentifier($where['column']),
1449
            $where['operator'],
1450
            $this->param($where['value'])
1451
        );
1452
    }
1453
1454
    /**
1455
     * @param array<string, mixed> $where
1456
     * @return string
1457
     */
1458
    protected function whereIn(array $where): string
1459
    {
1460
        return sprintf(
1461
            '%s %s (%s)',
1462
            $this->quoteIdentifier($where['column']),
1463
            $where['not'] ? 'NOT IN' : 'IN',
1464
            $this->params($where['value'])
1465
        );
1466
    }
1467
1468
    /**
1469
     * @param array<string, mixed> $where
1470
     * @return string
1471
     */
1472
    protected function whereInSelect(array $where): string
1473
    {
1474
        return sprintf(
1475
            '%s %s (%s)',
1476
            $this->quoteIdentifier($where['column']),
1477
            $where['not'] ? 'NOT IN' : 'IN',
1478
            $this->select($where['subquery']->getQueryStatement())
1479
        );
1480
    }
1481
1482
    /**
1483
     * @param array<string, mixed> $where
1484
     * @return string
1485
     */
1486
    protected function whereNested(array $where): string
1487
    {
1488
        return '(' . $this->getWheres($where['clause'], false) . ')';
1489
    }
1490
1491
    /**
1492
     * @param array<string, mixed> $where
1493
     * @return string
1494
     */
1495
    public function whereExists(array $where): string
1496
    {
1497
        return sprintf(
1498
            '%s (%s)',
1499
            $where['not'] ? 'NOT EXISTS' : 'EXISTS',
1500
            $this->select($where['subquery']->getQueryStatement())
1501
        );
1502
    }
1503
1504
    /**
1505
     * @param array<string, mixed> $where
1506
     * @return string
1507
     */
1508
    protected function whereNull(array $where): string
1509
    {
1510
        return sprintf(
1511
            '%s %s',
1512
            $this->quoteIdentifier($where['column']),
1513
            $where['not'] ? 'IS NOT NULL' : 'IS NULL',
1514
        );
1515
    }
1516
1517
    /**
1518
     * @param array<string, mixed> $where
1519
     * @return string
1520
     */
1521
    protected function whereBetween(array $where): string
1522
    {
1523
        return sprintf(
1524
            '%s %s %s AND %s',
1525
            $this->quoteIdentifier($where['column']),
1526
            $where['not'] ? 'NOT BETWEEN' : 'BETWEEN',
1527
            $this->param($where['value1']),
1528
            $this->param($where['value2']),
1529
        );
1530
    }
1531
1532
    /**
1533
     * @param array<string, mixed> $where
1534
     * @return string
1535
     */
1536
    protected function whereLike(array $where): string
1537
    {
1538
        return sprintf(
1539
            '%s %s %s',
1540
            $this->quoteIdentifier($where['column']),
1541
            $where['not'] ? 'NOT LIKE' : 'LIKE',
1542
            $this->param($where['pattern']),
1543
        );
1544
    }
1545
1546
    /**
1547
     * @param array<string, mixed> $where
1548
     * @return string
1549
     */
1550
    protected function whereSubQuery(array $where): string
1551
    {
1552
        return sprintf(
1553
            '%s %s (%s)',
1554
            $this->quoteIdentifier($where['column']),
1555
            $where['operator'],
1556
            $this->select($where['subquery']->getQueryStatement())
1557
        );
1558
    }
1559
1560
    /**
1561
     * @param array<string, mixed> $where
1562
     * @return string
1563
     */
1564
    protected function whereNop(array $where): string
1565
    {
1566
        return $this->quoteIdentifier($where['column']);
1567
    }
1568
1569
    /**
1570
     * @param array<string, mixed> $having
1571
     * @return string
1572
     */
1573
    protected function havingCondition(array $having): string
1574
    {
1575
        return sprintf(
1576
            '%s %s %s',
1577
            $this->quoteIdentifier($having['aggregate']),
1578
            $having['operator'],
1579
            $having['value']
1580
        );
1581
    }
1582
1583
    /**
1584
     * @param array<string, mixed> $having
1585
     * @return string
1586
     */
1587
    protected function havingNested(array $having): string
1588
    {
1589
        return '(' . $this->getHaving($having['conditions'], false) . ')';
1590
    }
1591
1592
    /**
1593
     * @param array<string, mixed> $having
1594
     * @return string
1595
     */
1596
    protected function havingBetween(array $having): string
1597
    {
1598
        return sprintf(
1599
            '%s %s %s AND %s',
1600
            $this->quoteIdentifier($having['aggregate']),
1601
            $having['not'] ? 'NOT BETWEEN' : 'BETWEEN',
1602
            $this->param($having['value1']),
1603
            $this->param($having['value2']),
1604
        );
1605
    }
1606
1607
    /**
1608
     * @param array<string, mixed> $having
1609
     * @return string
1610
     */
1611
    protected function havingInSelect(array $having): string
1612
    {
1613
        return sprintf(
1614
            '%s %s (%s)',
1615
            $this->quoteIdentifier($having['aggregate']),
1616
            $having['not'] ? 'NOT IN' : 'IN',
1617
            $this->select($having['subquery']->getQueryStatement())
1618
        );
1619
    }
1620
1621
    /**
1622
     * @param array<string, mixed> $having
1623
     * @return string
1624
     */
1625
    protected function havingIn(array $having): string
1626
    {
1627
        return sprintf(
1628
            '%s %s (%s)',
1629
            $this->quoteIdentifier($having['aggregate']),
1630
            $having['not'] ? 'NOT IN' : 'IN',
1631
            $this->params($having['value'])
1632
        );
1633
    }
1634
1635
    /**
1636
     * Return aggregate function COUNT
1637
     * @param array<string, mixed> $function
1638
     * @return string
1639
     */
1640
    protected function aggregateFunctionCOUNT(array $function): string
1641
    {
1642
        return sprintf(
1643
            'COUNT(%s%s)',
1644
            $function['distinct'] ? 'DISTINCT ' : '',
1645
            $this->columns($function['column'])
1646
        );
1647
    }
1648
1649
    /**
1650
     * Return aggregate function AVG
1651
     * @param array<string, mixed> $function
1652
     * @return string
1653
     */
1654
    protected function aggregateFunctionAVG(array $function): string
1655
    {
1656
        return sprintf(
1657
            'AVG(%s%s)',
1658
            $function['distinct'] ? 'DISTINCT ' : '',
1659
            $this->quoteIdentifier($function['column'])
1660
        );
1661
    }
1662
1663
    /**
1664
     * Return aggregate function SUM
1665
     * @param array<string, mixed> $function
1666
     * @return string
1667
     */
1668
    protected function aggregateFunctionSUM(array $function): string
1669
    {
1670
        return sprintf(
1671
            'SUM(%s%s)',
1672
            $function['distinct'] ? 'DISTINCT ' : '',
1673
            $this->quoteIdentifier($function['column'])
1674
        );
1675
    }
1676
1677
    /**
1678
     * Return aggregate function MIN
1679
     * @param array<string, mixed> $function
1680
     * @return string
1681
     */
1682
    protected function aggregateFunctionMIN(array $function): string
1683
    {
1684
        return sprintf(
1685
            'MIN(%s%s)',
1686
            $function['distinct'] ? 'DISTINCT ' : '',
1687
            $this->quoteIdentifier($function['column'])
1688
        );
1689
    }
1690
1691
    /**
1692
     * Return aggregate function MAX
1693
     * @param array<string, mixed> $function
1694
     * @return string
1695
     */
1696
    protected function aggregateFunctionMAX(array $function): string
1697
    {
1698
        return sprintf(
1699
            'MAX(%s%s)',
1700
            $function['distinct'] ? 'DISTINCT ' : '',
1701
            $this->quoteIdentifier($function['column'])
1702
        );
1703
    }
1704
}
1705