Passed
Push — develop ( b3554d...9f4fe4 )
by nguereza
02:32
created

Driver::alter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 19
rs 9.9332
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
{
68
    /**
69
     * The driver default date format
70
     * @var string
71
     */
72
    protected string $dateFormat = 'Y-m-d H:i:s';
73
74
    /**
75
     * The quote identifier for a table and columns
76
     * @var string
77
     */
78
    protected string $identifier = '"%s"';
79
80
    /**
81
     * Each query separator
82
     * @var string
83
     */
84
    protected string $separator = ';';
85
86
    /**
87
     * The columns modifiers
88
     * @var array<string>
89
     */
90
    protected array $modifiers = [
91
        'unsigned',
92
        'nullable',
93
        'default',
94
        'autoincrement',
95
        'description',
96
        'after',
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 getModifierAfter(BaseColumn $column): string
852
    {
853
        return $column->get('after', null) === null ? '' : 'AFTER ' . $this->quoteIdentifier($column->get('after'));
854
    }
855
856
    /**
857
     *
858
     * @param BaseColumn $column
859
     * @return string
860
     */
861
    protected function getModifierAutoincrement(BaseColumn $column): string
862
    {
863
        if (
864
            $column->getType() !== 'integer'
865
            || !in_array($column->get('size', 'normal'), $this->serials)
866
        ) {
867
            return '';
868
        }
869
        return $column->get('autoincrement', false) ? $this->autoincrement : '';
870
    }
871
872
    /**
873
     *
874
     * @param CreateTable $schema
875
     * @return string
876
     */
877
    protected function getPrimaryKey(CreateTable $schema): string
878
    {
879
        $primaryKey = $schema->getPrimaryKey();
880
        if (empty($primaryKey)) {
881
            return '';
882
        }
883
884
        return ",\n" . 'CONSTRAINT ' . $this->quoteIdentifier($primaryKey['name'])
885
                . ' PRIMARY KEY (' . $this->quoteIdentifiers($primaryKey['columns']) . ')';
886
    }
887
888
    /**
889
     *
890
     * @param CreateTable $schema
891
     * @return string
892
     */
893
    protected function getUniqueKeys(CreateTable $schema): string
894
    {
895
        $indexes = $schema->getUniqueKeys();
896
897
        if (empty($indexes)) {
898
            return '';
899
        }
900
901
        $sql = [];
902
903
        foreach ($indexes as $name => $columns) {
904
            $sql[] = 'CONSTRAINT ' . $this->quoteIdentifier($name)
905
                    . ' UNIQUE (' . $this->quoteIdentifiers($columns) . ')';
906
        }
907
908
        return ",\n" . implode(",\n", $sql);
909
    }
910
911
    /**
912
     *
913
     * @param CreateTable $schema
914
     * @return array<int, string>
915
     */
916
    protected function getIndexKeys(CreateTable $schema): array
917
    {
918
        $indexes = $schema->getIndexes();
919
920
        if (empty($indexes)) {
921
            return [];
922
        }
923
924
        $sql = [];
925
        $table = $this->quoteIdentifier($schema->getTableName());
926
927
        foreach ($indexes as $name => $columns) {
928
            $sql[] = 'CREATE INDEX ' . $this->quoteIdentifier($name)
929
                    . ' ON ' . $table . '(' . $this->quoteIdentifiers($columns) . ')';
930
        }
931
932
        return $sql;
933
    }
934
935
    /**
936
     *
937
     * @param CreateTable $schema
938
     * @return string
939
     */
940
    protected function getForeignKeys(CreateTable $schema): string
941
    {
942
        $keys = $schema->getForeignKeys();
943
944
        if (empty($keys)) {
945
            return '';
946
        }
947
948
        $sql = [];
949
950
        foreach ($keys as $name => $key) {
951
            $cmd = 'CONSTRAINT ' . $this->quoteIdentifier($name)
952
                    . ' FOREIGN KEY (' . $this->quoteIdentifiers($key->getColumns()) . ') ';
953
            $cmd .= 'REFERENCES ' . $this->quoteIdentifier($key->getReferenceTable())
954
                    . ' (' . $this->quoteIdentifiers($key->getReferenceColumns()) . ')';
955
956
            foreach ($key->getActions() as $actionName => $action) {
957
                $cmd .= ' ' . $actionName . ' ' . $action;
958
            }
959
            $sql[] = $cmd;
960
        }
961
962
        return ",\n" . implode(",\n", $sql);
963
    }
964
965
    /**
966
     *
967
     * @param CreateTable $schema
968
     * @return string
969
     */
970
    protected function getEngine(CreateTable $schema): string
971
    {
972
        $engine = $schema->getEngine();
973
        if ($engine === null) {
974
            return '';
975
        }
976
977
        return ' ENGINE = ' . strtoupper($engine);
978
    }
979
980
    /**
981
     *
982
     * @param AlterTable $schema
983
     * @param mixed $data
984
     * @return string
985
     */
986
    protected function getDropPrimaryKey(AlterTable $schema, $data): string
987
    {
988
        return sprintf(
989
            'ALTER TABLE %s DROP CONSTRAINT %s',
990
            $this->quoteIdentifier($schema->getTableName()),
991
            $this->quoteIdentifier($data)
992
        );
993
    }
994
995
    /**
996
     *
997
     * @param AlterTable $schema
998
     * @param mixed $data
999
     * @return string
1000
     */
1001
    protected function getDropUniqueKey(AlterTable $schema, $data): string
1002
    {
1003
        return sprintf(
1004
            'ALTER TABLE %s DROP CONSTRAINT %s',
1005
            $this->quoteIdentifier($schema->getTableName()),
1006
            $this->quoteIdentifier($data)
1007
        );
1008
    }
1009
1010
    /**
1011
     *
1012
     * @param AlterTable $schema
1013
     * @param mixed $data
1014
     * @return string
1015
     */
1016
    protected function getDropIndex(AlterTable $schema, $data): string
1017
    {
1018
        return sprintf(
1019
            'DROP INDEX %s.%s',
1020
            $this->quoteIdentifier($schema->getTableName()),
1021
            $this->quoteIdentifier($data)
1022
        );
1023
    }
1024
1025
    /**
1026
     *
1027
     * @param AlterTable $schema
1028
     * @param mixed $data
1029
     * @return string
1030
     */
1031
    protected function getDropForeignKey(AlterTable $schema, $data): string
1032
    {
1033
        return sprintf(
1034
            'ALTER TABLE %s DROP CONSTRAINT %s',
1035
            $this->quoteIdentifier($schema->getTableName()),
1036
            $this->quoteIdentifier($data)
1037
        );
1038
    }
1039
1040
    /**
1041
     *
1042
     * @param AlterTable $schema
1043
     * @param mixed $data
1044
     * @return string
1045
     */
1046
    protected function getDropColumn(AlterTable $schema, $data): string
1047
    {
1048
        return sprintf(
1049
            'ALTER TABLE %s DROP COLUMN %s',
1050
            $this->quoteIdentifier($schema->getTableName()),
1051
            $this->quoteIdentifier($data)
1052
        );
1053
    }
1054
1055
    /**
1056
     *
1057
     * @param AlterTable $schema
1058
     * @param mixed $data
1059
     * @return string
1060
     */
1061
    protected function getRenameColumn(AlterTable $schema, $data): string
1062
    {
1063
        //TODO: please implement it in subclass
1064
        return '';
1065
    }
1066
1067
    /**
1068
     *
1069
     * @param AlterTable $schema
1070
     * @param mixed $data
1071
     * @return string
1072
     */
1073
    protected function getModifyColumn(AlterTable $schema, $data): string
1074
    {
1075
        return sprintf(
1076
            'ALTER TABLE %s MODIFY COLUMN %s',
1077
            $this->quoteIdentifier($schema->getTableName()),
1078
            $this->getSchemaColumns([$data])
1079
        );
1080
    }
1081
1082
    /**
1083
     *
1084
     * @param AlterTable $schema
1085
     * @param mixed $data
1086
     * @return string
1087
     */
1088
    protected function getAddColumn(AlterTable $schema, $data): string
1089
    {
1090
        return sprintf(
1091
            'ALTER TABLE %s ADD COLUMN %s',
1092
            $this->quoteIdentifier($schema->getTableName()),
1093
            $this->getSchemaColumns([$data])
1094
        );
1095
    }
1096
1097
    /**
1098
     *
1099
     * @param AlterTable $schema
1100
     * @param mixed $data
1101
     * @return string
1102
     */
1103
    protected function getAddPrimary(AlterTable $schema, $data): string
1104
    {
1105
        return sprintf(
1106
            'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)',
1107
            $this->quoteIdentifier($schema->getTableName()),
1108
            $this->quoteIdentifier($data['name']),
1109
            $this->quoteIdentifiers($data['columns'])
1110
        );
1111
    }
1112
1113
    /**
1114
     *
1115
     * @param AlterTable $schema
1116
     * @param mixed $data
1117
     * @return string
1118
     */
1119
    protected function getAddUnique(AlterTable $schema, $data): string
1120
    {
1121
        return sprintf(
1122
            'ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)',
1123
            $this->quoteIdentifier($schema->getTableName()),
1124
            $this->quoteIdentifier($data['name']),
1125
            $this->quoteIdentifiers($data['columns'])
1126
        );
1127
    }
1128
1129
    /**
1130
     *
1131
     * @param AlterTable $schema
1132
     * @param mixed $data
1133
     * @return string
1134
     */
1135
    protected function getAddIndex(AlterTable $schema, $data): string
1136
    {
1137
        return sprintf(
1138
            'CREATE INDEX %s ON %s (%s)',
1139
            $this->quoteIdentifier($data['name']),
1140
            $this->quoteIdentifier($schema->getTableName()),
1141
            $this->quoteIdentifiers($data['columns'])
1142
        );
1143
    }
1144
1145
    /**
1146
     *
1147
     * @param AlterTable $schema
1148
     * @param mixed $data
1149
     * @return string
1150
     */
1151
    protected function getAddForeign(AlterTable $schema, $data): string
1152
    {
1153
        /** @var ForeignKey $key */
1154
        $key = $data['foreign'];
1155
        return sprintf(
1156
            'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)',
1157
            $this->quoteIdentifier($schema->getTableName()),
1158
            $this->quoteIdentifier($data['name']),
1159
            $this->quoteIdentifiers($key->getColumns()),
1160
            $this->quoteIdentifier($key->getReferenceTable()),
1161
            $this->quoteIdentifiers($key->getReferenceColumns()),
1162
        );
1163
    }
1164
1165
    /**
1166
     *
1167
     * @param AlterTable $schema
1168
     * @param mixed $data
1169
     * @return string
1170
     */
1171
    protected function getSetDefaultValue(AlterTable $schema, $data): string
1172
    {
1173
        return sprintf(
1174
            'ALTER TABLE %s ALTER COLUMN %s SET DEFAULT (%s)',
1175
            $this->quoteIdentifier($schema->getTableName()),
1176
            $this->quoteIdentifier($data['column']),
1177
            $this->value($data['value'])
1178
        );
1179
    }
1180
1181
    /**
1182
     *
1183
     * @param AlterTable $schema
1184
     * @param mixed $data
1185
     * @return string
1186
     */
1187
    protected function getDropDefaultValue(AlterTable $schema, $data): string
1188
    {
1189
        return sprintf(
1190
            'ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
1191
            $this->quoteIdentifier($schema->getTableName()),
1192
            $this->quoteIdentifier($data)
1193
        );
1194
    }
1195
1196
    /**
1197
     * Handle into the table
1198
     *
1199
     * @param string|null $table
1200
     *
1201
     * @return string
1202
     */
1203
    protected function getInto(?string $table): string
1204
    {
1205
        if ($table === null) {
1206
            return '';
1207
        }
1208
        return ' INTO ' . $this->quoteIdentifier($table);
1209
    }
1210
1211
    /**
1212
     * Handle tables
1213
     * @param array<mixed, string> $tables
1214
     *
1215
     * @return string
1216
     */
1217
    protected function getTableList(array $tables): string
1218
    {
1219
        if (empty($tables)) {
1220
            return '';
1221
        }
1222
        $sql = [];
1223
        foreach ($tables as $name => $alias) {
1224
            if (is_string($name)) {
1225
                $sql[] = $this->quoteIdentifier($name) . ' AS ' . $this->quoteIdentifier($alias);
1226
            } else {
1227
                $sql[] = $this->quoteIdentifier($alias);
1228
            }
1229
        }
1230
1231
        return implode(', ', $sql);
1232
    }
1233
1234
    /**
1235
     * Handle for joins
1236
     * @param array<int, mixed> $joins
1237
     *
1238
     * @return string
1239
     */
1240
    protected function getJoins(array $joins): string
1241
    {
1242
        if (empty($joins)) {
1243
            return '';
1244
        }
1245
        $sql = [];
1246
1247
        foreach ($joins as $join) {
1248
            $joinObject = $join['join'];
1249
1250
            $on = '';
1251
            if ($joinObject) {
1252
                $on = $this->getJoinConditions($joinObject->getJoinConditions());
1253
            }
1254
1255
            if ($on !== '') {
1256
                $on = ' ON ' . $on;
1257
            }
1258
1259
            $sql[] = $join['type'] . ' JOIN ' . $this->getTableList($join['table']) . $on;
1260
        }
1261
1262
        return ' ' . implode(' ', $sql);
1263
    }
1264
1265
    /**
1266
     * Handle for the join conditions
1267
     * @param array<int, mixed> $conditions
1268
     * @return string
1269
     */
1270
    protected function getJoinConditions(array $conditions): string
1271
    {
1272
        if (empty($conditions)) {
1273
            return '';
1274
        }
1275
1276
        $sql = [];
1277
1278
        $sql[] = $this->{$conditions[0]['type']}($conditions[0]);
1279
1280
        $count = count($conditions);
1281
        for ($i = 1; $i < $count; $i++) {
1282
            $sql[] = $conditions[$i]['separator'] . ' ' . $this->{$conditions[$i]['type']}($conditions[$i]);
1283
        }
1284
1285
        return implode(' ', $sql);
1286
    }
1287
1288
    /**
1289
     * Handle group by
1290
     * @param Expression[]|string[] $groupBy
1291
     *
1292
     * @return string
1293
     */
1294
    protected function getGroupBy(array $groupBy): string
1295
    {
1296
        return empty($groupBy) ? '' : ' GROUP BY ' . $this->columns($groupBy);
1297
    }
1298
1299
    /**
1300
     * Handle for Order
1301
     * @param array<int, array<string, mixed>> $orders
1302
     * @return string
1303
     */
1304
    protected function getOrders(array $orders): string
1305
    {
1306
        if (empty($orders)) {
1307
            return '';
1308
        }
1309
        $sql = [];
1310
        foreach ($orders as $order) {
1311
            $sql[] = $this->columns($order['columns']) . ' ' . $order['order'];
1312
        }
1313
1314
        return ' ORDER BY ' . implode(', ', $sql);
1315
    }
1316
1317
    /**
1318
     * Handle columns for set (UPDATE)
1319
     * @param array<int, array<string, mixed>> $columns
1320
     * @return string
1321
     */
1322
    protected function getSetColumns(array $columns): string
1323
    {
1324
        if (empty($columns)) {
1325
            return '';
1326
        }
1327
        $sql = [];
1328
1329
        foreach ($columns as $column) {
1330
            $sql[] = $this->quoteIdentifier($column['column']) . ' = ' . $this->param($column['value']);
1331
        }
1332
1333
        return ' SET ' . implode(', ', $sql);
1334
    }
1335
1336
    /**
1337
     * Handler where
1338
     * @param array<int, mixed> $wheres
1339
     * @param bool $prefix
1340
     *
1341
     * @return string
1342
     */
1343
    protected function getWheres(array $wheres, bool $prefix = true): string
1344
    {
1345
        $sql = $this->getWheresHaving($wheres);
1346
        if (empty($sql)) {
1347
            return '';
1348
        }
1349
        return ($prefix ? ' WHERE ' : '') . $sql;
1350
    }
1351
1352
    /**
1353
     * Handle for having
1354
     * @param array<int, mixed> $having
1355
     * @param bool $prefix
1356
     * @return string
1357
     */
1358
    protected function getHaving(array $having, bool $prefix = true): string
1359
    {
1360
        $sql = $this->getWheresHaving($having);
1361
        if (empty($sql)) {
1362
            return '';
1363
        }
1364
        return ($prefix ? ' HAVING ' : '') . $sql;
1365
    }
1366
1367
    /**
1368
     * Return the build part for where or having
1369
     * @param array<int, mixed> $values
1370
     *
1371
     * @return string
1372
     */
1373
    protected function getWheresHaving(array $values): string
1374
    {
1375
        if (empty($values)) {
1376
            return '';
1377
        }
1378
        $sql = [];
1379
        $sql[] = $this->{$values[0]['type']}($values[0]);
1380
        $count = count($values);
1381
1382
        for ($i = 1; $i < $count; $i++) {
1383
            $sql[] = $values[$i]['separator'] . ' ' . $this->{$values[$i]['type']}($values[$i]);
1384
        }
1385
        return implode(' ', $sql);
1386
    }
1387
1388
    /**
1389
     * Handle for insert values
1390
     * @param array<int, mixed> $values
1391
     * @return string
1392
     */
1393
    protected function getInsertValues(array $values): string
1394
    {
1395
        return ' VALUES (' . $this->params($values) . ')';
1396
    }
1397
1398
    /**
1399
     * Handle for limit
1400
     * @param int $limit
1401
     * @return string
1402
     */
1403
    protected function getLimit(int $limit): string
1404
    {
1405
        return ($limit === 0) ? '' : ' LIMIT ' . $this->param($limit);
1406
    }
1407
1408
    /**
1409
     * Handle for offset
1410
     * @param int $offset
1411
     * @return string
1412
     */
1413
    protected function getOffset(int $offset): string
1414
    {
1415
        return ($offset < 0) ? '' : ' OFFSET ' . $this->param($offset);
1416
    }
1417
1418
    /**
1419
     * @param array<string, mixed> $join
1420
     * @return string
1421
     */
1422
    protected function joinColumn(array $join): string
1423
    {
1424
        return sprintf(
1425
            '%s %s %s',
1426
            $this->quoteIdentifier($join['column1']),
1427
            $join['operator'],
1428
            $this->quoteIdentifier($join['column2'])
1429
        );
1430
    }
1431
1432
    /**
1433
     * @param array<string, mixed> $join
1434
     * @return string
1435
     */
1436
    protected function joinNested(array $join): string
1437
    {
1438
        return '(' . $this->getJoinConditions($join['join']->getJoinConditions()) . ')';
1439
    }
1440
1441
    /**
1442
     * @param array<string, mixed> $join
1443
     * @return string
1444
     */
1445
    protected function joinExpression(array $join): string
1446
    {
1447
        return $this->quoteIdentifier($join['expression']);
1448
    }
1449
1450
    /**
1451
     * @param array<string, mixed> $where
1452
     * @return string
1453
     */
1454
    protected function whereColumn(array $where): string
1455
    {
1456
        return sprintf(
1457
            '%s %s %s',
1458
            $this->quoteIdentifier($where['column']),
1459
            $where['operator'],
1460
            $this->param($where['value'])
1461
        );
1462
    }
1463
1464
    /**
1465
     * @param array<string, mixed> $where
1466
     * @return string
1467
     */
1468
    protected function whereIn(array $where): string
1469
    {
1470
        return sprintf(
1471
            '%s %s (%s)',
1472
            $this->quoteIdentifier($where['column']),
1473
            $where['not'] ? 'NOT IN' : 'IN',
1474
            $this->params($where['value'])
1475
        );
1476
    }
1477
1478
    /**
1479
     * @param array<string, mixed> $where
1480
     * @return string
1481
     */
1482
    protected function whereInSelect(array $where): string
1483
    {
1484
        return sprintf(
1485
            '%s %s (%s)',
1486
            $this->quoteIdentifier($where['column']),
1487
            $where['not'] ? 'NOT IN' : 'IN',
1488
            $this->select($where['subquery']->getQueryStatement())
1489
        );
1490
    }
1491
1492
    /**
1493
     * @param array<string, mixed> $where
1494
     * @return string
1495
     */
1496
    protected function whereNested(array $where): string
1497
    {
1498
        return '(' . $this->getWheres($where['clause'], false) . ')';
1499
    }
1500
1501
    /**
1502
     * @param array<string, mixed> $where
1503
     * @return string
1504
     */
1505
    public function whereExists(array $where): string
1506
    {
1507
        return sprintf(
1508
            '%s (%s)',
1509
            $where['not'] ? 'NOT EXISTS' : 'EXISTS',
1510
            $this->select($where['subquery']->getQueryStatement())
1511
        );
1512
    }
1513
1514
    /**
1515
     * @param array<string, mixed> $where
1516
     * @return string
1517
     */
1518
    protected function whereNull(array $where): string
1519
    {
1520
        return sprintf(
1521
            '%s %s',
1522
            $this->quoteIdentifier($where['column']),
1523
            $where['not'] ? 'IS NOT NULL' : 'IS NULL',
1524
        );
1525
    }
1526
1527
    /**
1528
     * @param array<string, mixed> $where
1529
     * @return string
1530
     */
1531
    protected function whereBetween(array $where): string
1532
    {
1533
        return sprintf(
1534
            '%s %s %s AND %s',
1535
            $this->quoteIdentifier($where['column']),
1536
            $where['not'] ? 'NOT BETWEEN' : 'BETWEEN',
1537
            $this->param($where['value1']),
1538
            $this->param($where['value2']),
1539
        );
1540
    }
1541
1542
    /**
1543
     * @param array<string, mixed> $where
1544
     * @return string
1545
     */
1546
    protected function whereLike(array $where): string
1547
    {
1548
        return sprintf(
1549
            '%s %s %s',
1550
            $this->quoteIdentifier($where['column']),
1551
            $where['not'] ? 'NOT LIKE' : 'LIKE',
1552
            $this->param($where['pattern']),
1553
        );
1554
    }
1555
1556
    /**
1557
     * @param array<string, mixed> $where
1558
     * @return string
1559
     */
1560
    protected function whereSubQuery(array $where): string
1561
    {
1562
        return sprintf(
1563
            '%s %s (%s)',
1564
            $this->quoteIdentifier($where['column']),
1565
            $where['operator'],
1566
            $this->select($where['subquery']->getQueryStatement())
1567
        );
1568
    }
1569
1570
    /**
1571
     * @param array<string, mixed> $where
1572
     * @return string
1573
     */
1574
    protected function whereNop(array $where): string
1575
    {
1576
        return $this->quoteIdentifier($where['column']);
1577
    }
1578
1579
    /**
1580
     * @param array<string, mixed> $having
1581
     * @return string
1582
     */
1583
    protected function havingCondition(array $having): string
1584
    {
1585
        return sprintf(
1586
            '%s %s %s',
1587
            $this->quoteIdentifier($having['aggregate']),
1588
            $having['operator'],
1589
            $having['value']
1590
        );
1591
    }
1592
1593
    /**
1594
     * @param array<string, mixed> $having
1595
     * @return string
1596
     */
1597
    protected function havingNested(array $having): string
1598
    {
1599
        return '(' . $this->getHaving($having['conditions'], false) . ')';
1600
    }
1601
1602
    /**
1603
     * @param array<string, mixed> $having
1604
     * @return string
1605
     */
1606
    protected function havingBetween(array $having): string
1607
    {
1608
        return sprintf(
1609
            '%s %s %s AND %s',
1610
            $this->quoteIdentifier($having['aggregate']),
1611
            $having['not'] ? 'NOT BETWEEN' : 'BETWEEN',
1612
            $this->param($having['value1']),
1613
            $this->param($having['value2']),
1614
        );
1615
    }
1616
1617
    /**
1618
     * @param array<string, mixed> $having
1619
     * @return string
1620
     */
1621
    protected function havingInSelect(array $having): string
1622
    {
1623
        return sprintf(
1624
            '%s %s (%s)',
1625
            $this->quoteIdentifier($having['aggregate']),
1626
            $having['not'] ? 'NOT IN' : 'IN',
1627
            $this->select($having['subquery']->getQueryStatement())
1628
        );
1629
    }
1630
1631
    /**
1632
     * @param array<string, mixed> $having
1633
     * @return string
1634
     */
1635
    protected function havingIn(array $having): string
1636
    {
1637
        return sprintf(
1638
            '%s %s (%s)',
1639
            $this->quoteIdentifier($having['aggregate']),
1640
            $having['not'] ? 'NOT IN' : 'IN',
1641
            $this->params($having['value'])
1642
        );
1643
    }
1644
1645
    /**
1646
     * Return aggregate function COUNT
1647
     * @param array<string, mixed> $function
1648
     * @return string
1649
     */
1650
    protected function aggregateFunctionCOUNT(array $function): string
1651
    {
1652
        return sprintf(
1653
            'COUNT(%s%s)',
1654
            $function['distinct'] ? 'DISTINCT ' : '',
1655
            $this->columns($function['column'])
1656
        );
1657
    }
1658
1659
    /**
1660
     * Return aggregate function AVG
1661
     * @param array<string, mixed> $function
1662
     * @return string
1663
     */
1664
    protected function aggregateFunctionAVG(array $function): string
1665
    {
1666
        return sprintf(
1667
            'AVG(%s%s)',
1668
            $function['distinct'] ? 'DISTINCT ' : '',
1669
            $this->quoteIdentifier($function['column'])
1670
        );
1671
    }
1672
1673
    /**
1674
     * Return aggregate function SUM
1675
     * @param array<string, mixed> $function
1676
     * @return string
1677
     */
1678
    protected function aggregateFunctionSUM(array $function): string
1679
    {
1680
        return sprintf(
1681
            'SUM(%s%s)',
1682
            $function['distinct'] ? 'DISTINCT ' : '',
1683
            $this->quoteIdentifier($function['column'])
1684
        );
1685
    }
1686
1687
    /**
1688
     * Return aggregate function MIN
1689
     * @param array<string, mixed> $function
1690
     * @return string
1691
     */
1692
    protected function aggregateFunctionMIN(array $function): string
1693
    {
1694
        return sprintf(
1695
            'MIN(%s%s)',
1696
            $function['distinct'] ? 'DISTINCT ' : '',
1697
            $this->quoteIdentifier($function['column'])
1698
        );
1699
    }
1700
1701
    /**
1702
     * Return aggregate function MAX
1703
     * @param array<string, mixed> $function
1704
     * @return string
1705
     */
1706
    protected function aggregateFunctionMAX(array $function): string
1707
    {
1708
        return sprintf(
1709
            'MAX(%s%s)',
1710
            $function['distinct'] ? 'DISTINCT ' : '',
1711
            $this->quoteIdentifier($function['column'])
1712
        );
1713
    }
1714
}
1715