Passed
Push — develop ( a25cb7...a43744 )
by nguereza
03:28
created

Driver::params()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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