Passed
Pull Request — master (#15)
by Todd
02:41
created

MySqlDb::getColumnOrders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
ccs 0
cts 7
cp 0
crap 6
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Db\Drivers;
9
10
use Garden\Db\Db;
11
use Garden\Db\Identifier;
12
use Garden\Db\Literal;
13
use PDO;
14
15
/**
16
 * A {@link Db} class for connecting to MySQL.
17
 */
18
class MySqlDb extends Db {
19
    const MYSQL_DATE_FORMAT = 'Y-m-d H:i:s';
20
21
    /**
22
     * @var string
23
     */
24
    protected $dbname;
25
26
    protected static $map = [
27
        Db::OP_GT => '>',
28
        Db::OP_GTE => '>=',
29
        Db::OP_LT => '<',
30
        Db::OP_LTE => '<=',
31
        Db::OP_LIKE => 'like',
32
        Db::OP_AND => 'and',
33
        Db::OP_OR => 'or',
34
    ];
35
36
    /**
37
     * {@inheritdoc}
38
     */
39 9
    protected function dropTableDb(string $table, array $options = []) {
40
        $sql = 'drop table '.
41 9
            ($options[Db::OPTION_IGNORE] ? 'if exists ' : '').
42 9
            $this->prefixTable($table);
43
44 9
        $this->queryDefine($sql);
45 9
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 7
    protected function fetchTableDefDb(string $table) {
51 7
        $columns = $this->fetchColumnDefsDb($table);
52
53 7
        if (empty($columns)) {
54
            // A table with no columns does not exist.
55 5
            return null;
56
        }
57
58 6
        $indexes = $this->fetchIndexesDb($table);
59
60
        $tableDef = [
61 6
            'name' => $table,
62 6
            'columns' => $columns,
63 6
            'indexes' => $indexes
64
        ];
65
66 6
        return $tableDef;
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    protected function fetchColumnDefsDb(string $table) {
73
        $rows = $this->get(
74
            new Identifier('information_schema', 'COLUMNS'),
75
            [
76
                'TABLE_SCHEMA' => $this->getDbName(),
77
                'TABLE_NAME' => $this->prefixTable($table, false)
78
            ],
79
            [
80
                Db::OPTION_FETCH_MODE => PDO::FETCH_ASSOC,
81
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
82
            ]
83
        );
84
85
        $columns = [];
86
        foreach ($rows as $row) {
87
            $columnType = $row['COLUMN_TYPE'];
88
            if ($columnType === 'tinyint(1)') {
89
                $columnType = 'bool';
90
            }
91
            $column = Db::typeDef($columnType);
92
            if ($column === null) {
93
                throw new \Exception("Unknown type '$columnType'.", 500);
94
            }
95
96
            $column['allowNull'] = strcasecmp($row['IS_NULLABLE'], 'YES') === 0;
97
98
            if (($default = $row['COLUMN_DEFAULT']) !== null) {
99
                $column['default'] = $this->forceType($default, $column['type']);
100
            }
101
102
            if ($row['EXTRA'] === 'auto_increment') {
103
                $column['autoIncrement'] = true;
104
            }
105
106
            if ($row['COLUMN_KEY'] === 'PRI') {
107
                $column['primary'] = true;
108
            }
109
110
            $columns[$row['COLUMN_NAME']] = $column;
111
        }
112
113
        return $columns;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 34
    public function get($table, array $where, array $options = []): \PDOStatement {
120 34
        $sql = $this->buildSelect($table, $where, $options);
121 34
        $result = $this->query($sql, [], $options);
122 34
        return $result;
123
    }
124
125
    /**
126
     * Build a sql select statement.
127
     *
128
     * @param string|Identifier $table The name of the main table.
129
     * @param array $where The where filter.
130
     * @param array $options An array of additional query options.
131
     * @return string Returns the select statement as a string.
132
     * @see Db::get()
133
     */
134 37
    protected function buildSelect($table, array $where, array $options = []) {
135 37
        $options += ['limit' => 0];
136
137 37
        $sql = '';
138
139
        // Build the select clause.
140 37
        if (!empty($options['columns'])) {
141 10
            $columns = array();
142 10
            foreach ($options['columns'] as $value) {
143 10
                $columns[] = $this->escape($value);
144
            }
145 10
            $sql .= 'select '.implode(', ', $columns);
146
        } else {
147 28
            $sql .= "select *";
148
        }
149
150
        // Build the from clause.
151 37
        if ($table instanceof Literal) {
0 ignored issues
show
introduced by
$table is never a sub-type of Garden\Db\Literal.
Loading history...
152
            $table = $table->getValue($this);
153
        } else {
154 37
            $table = $this->prefixTable($table);
155
        }
156 37
        $sql .= "\nfrom $table";
157
158
        // Build the where clause.
159 37
        $whereString = $this->buildWhere($where, Db::OP_AND);
160 37
        if ($whereString) {
161 29
            $sql .= "\nwhere ".$whereString;
162
        }
163
164
        // Build the order.
165 37
        if (!empty($options['order'])) {
166 16
            $orders = [];
167 16
            foreach ($options['order'] as $column) {
168 16
                if ($column[0] === '-') {
169 1
                    $order = $this->escape(substr($column, 1)).' desc';
170
                } else {
171 16
                    $order = $this->escape($column);
172
                }
173 16
                $orders[] = $order;
174
            }
175 16
            $sql .= "\norder by ".implode(', ', $orders);
176
        }
177
178
        // Build the limit, offset.
179 37
        if (!empty($options['limit'])) {
180
            $limit = (int)$options['limit'];
181
            $sql .= "\nlimit $limit";
182
        }
183
184 37
        if (!empty($options['offset'])) {
185
            $sql .= ' offset '.((int)$options['offset']);
186 37
        } elseif (isset($options['page'])) {
187
            $offset = $options['limit'] * ($options['page'] - 1);
188
            $sql .= ' offset '.$offset;
189
        }
190
191 37
        return $sql;
192
    }
193
194
    /**
195
     * Build a where clause from a where array.
196
     *
197
     * @param array $where There where string.
198
     * This is an array in the form `['column' => 'value']` with more advanced options for non-equality comparisons.
199
     * @param string $op The logical operator to join multiple field comparisons.
200
     * @return string The where string.
201
     */
202 39
    protected function buildWhere($where, $op = Db::OP_AND) {
203 39
        $map = static::$map;
204 39
        $strop = $map[$op];
205
206 39
        $result = '';
207 39
        foreach ($where as $column => $value) {
208 31
            $btcolumn = $this->escape($column);
209
210 31
            if (is_array($value)) {
211 21
                if (is_numeric($column)) {
212
                    // This is a bracketed expression.
213 2
                    $result .= (empty($result) ? '' : "\n  $strop ").
214 2
                        "(\n  ".
215 2
                        $this->buildWhere($value, $op).
216 2
                        "\n  )";
217 21
                } elseif (in_array($column, [Db::OP_AND, Db::OP_OR])) {
218
                    // This is an AND/OR expression.
219 2
                    $result .= (empty($result) ? '' : "\n  $strop ").
220 2
                        "(\n  ".
221 2
                        $this->buildWhere($value, $column).
222 2
                        "\n  )";
223
                } else {
224 19
                    if (isset($value[0])) {
225
                        // This is a short in syntax.
226 1
                        $value = [Db::OP_IN => $value];
227
                    }
228
229 21
                    foreach ($value as $vop => $rval) {
230 19
                        if ($result) {
231 8
                            $result .= "\n  $strop ";
232
                        }
233
234 19
                        switch ($vop) {
235
                            case Db::OP_AND:
236
                            case Db::OP_OR:
237 2
                                if (is_numeric($column)) {
238
                                    $innerWhere = $rval;
239
                                } else {
240 2
                                    $innerWhere = [$column => $rval];
241
                                }
242
                                $result .= "(\n  ".
243 2
                                    $this->buildWhere($innerWhere, $vop).
244 2
                                    "\n  )";
245 2
                                break;
246
                            case Db::OP_EQ:
247 3
                                if ($rval === null) {
248
                                    $result .= "$btcolumn is null";
249 3
                                } elseif (is_array($rval)) {
250 1
                                    $result .= "$btcolumn in ".$this->bracketList($rval);
251
                                } else {
252 2
                                    $result .= "$btcolumn = ".$this->quote($rval);
253
                                }
254 3
                                break;
255
                            case Db::OP_GT:
256
                            case Db::OP_GTE:
257
                            case Db::OP_LT:
258
                            case Db::OP_LTE:
259 6
                                $result .= "$btcolumn {$map[$vop]} ".$this->quote($rval);
260 6
                                break;
261
                            case Db::OP_LIKE:
262 6
                                $result .= $this->buildLike($btcolumn, $rval);
263 6
                                break;
264
                            case Db::OP_IN:
265
                                // Quote the in values.
266 2
                                $rval = array_map([$this, 'quote'], (array)$rval);
267 2
                                $result .= "$btcolumn in (".implode(', ', $rval).')';
268 2
                                break;
269
                            case Db::OP_NEQ:
270 3
                                if ($rval === null) {
271 1
                                    $result .= "$btcolumn is not null";
272 2
                                } elseif (is_array($rval)) {
273 1
                                    $result .= "$btcolumn not in ".$this->bracketList($rval);
274
                                } else {
275 1
                                    $result .= "$btcolumn <> ".$this->quote($rval);
276
                                }
277 3
                                break;
278
                        }
279
                    }
280
                }
281
            } else {
282 18
                if ($result) {
283 2
                    $result .= "\n  $strop ";
284
                }
285
286
                // This is just an equality operator.
287 18
                if ($value === null) {
288 1
                    $result .= "$btcolumn is null";
289
                } else {
290 17
                    $result .= "$btcolumn = ".$this->quote($value);
291
                }
292
            }
293
        }
294 39
        return $result;
295
    }
296
297
    /**
298
     * Build a like expression.
299
     *
300
     * @param string $column The column name.
301
     * @param mixed $value The right-hand value.
302
     * @return string Returns the like expression.
303
     */
304
    protected function buildLike(string $column, $value): string {
305
        return "$column like ".$this->quote($value);
306
    }
307
308
    /**
309
     * Convert an array into a bracketed list suitable for MySQL clauses.
310
     *
311
     * @param array $row The row to expand.
312
     * @param string $quote The quotes to surroud the items with. There are two special cases.
313
     * ' (single quote)
314
     * : The row will be passed through {@link PDO::quote()}.
315
     * ` (backticks)
316
     * : The row will be passed through {@link MySqlDb::backtick()}.
317
     * @return string Returns the bracket list.
318
     */
319 40
    public function bracketList($row, $quote = "'") {
320 40
        switch ($quote) {
321 40
            case "'":
322 31
                $row = array_map([$this, 'quote'], $row);
323 31
                $quote = '';
324 31
                break;
325 40
            case '`':
326 40
                $row = array_map([$this, 'escape'], $row);
327 40
                $quote = '';
328 40
                break;
329
        }
330
331 40
        return "($quote".implode("$quote, $quote", $row)."$quote)";
332
    }
333
334
335
    /**
336
     * Get the current database name.
337
     *
338
     * @return mixed
339
     */
340
    private function getDbName() {
341
        if (!isset($this->dbname)) {
342
            $this->dbname = $this->getPDO()->query('select database()')->fetchColumn();
343
        }
344
        return $this->dbname;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350
    protected function nativeDbType(array $type) {
351
        static $translations = ['bool' => 'tinyint(1)', 'byte' => 'tinyint', 'short' => 'smallint', 'long' => 'bigint'];
352
353
        // Translate the dbtype to a MySQL native type.
354
        if (isset($translations[$type['dbtype']])) {
355
            $type['dbtype'] = $translations[$type['dbtype']];
356
        }
357
358
        // Unsigned is represented differently in MySQL.
359
        $unsigned = !empty($type['unsigned']);
360
        unset($type['unsigned']);
361
362
        // The max length is not specified for text columns.
363
        if (in_array($type['dbtype'], ['tinytext', 'text', 'mediumtext', 'longtext'])) {
364
            unset($type['maxLength']);
365
        }
366
367
        $dbType = static::dbType($type).($unsigned ? ' unsigned' : '');
368
369
        return $dbType;
370
    }
371
372
    /**
373
     * Parse a column type string and return it in a way that is suitable for a create/alter table statement.
374
     *
375
     * @param string $typeString The string to parse.
376
     * @return string Returns a canonical string.
377
     */
378
    protected function columnTypeString($typeString) {
379
        $type = null;
380
381
        if (substr($typeString, 0, 4) === 'enum') {
382
            // This is an enum which will come in as an array.
383
            if (preg_match_all("`'([^']+)'`", $typeString, $matches)) {
384
                $type = $matches[1];
385
            }
386
        } else {
387
            if (preg_match('`([a-z]+)\s*(?:\((\d+(?:\s*,\s*\d+)*)\))?\s*(unsigned)?`', $typeString, $matches)) {
388
                //         var_dump($matches);
389
                $str = $matches[1];
390
                $length = self::val(2, $matches);
391
                $unsigned = self::val(3, $matches);
392
393
                if (substr($str, 0, 1) == 'u') {
394
                    $unsigned = true;
395
                    $str = substr($str, 1);
396
                }
397
398
                // Remove the length from types without real lengths.
399
                if (in_array($str, array('tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'float', 'double'))) {
400
                    $length = null;
401
                }
402
403
                $type = $str;
404
                if ($length) {
405
                    $length = str_replace(' ', '', $length);
406
                    $type .= "($length)";
407
                }
408
                if ($unsigned) {
409
                    $type .= ' unsigned';
410
                }
411
            }
412
        }
413
414
        if (!$type) {
415
            debug_print_backtrace();
416
            trigger_error("Couldn't parse type $typeString", E_USER_ERROR);
417
        }
418
419
        return $type;
420
    }
421
422
    /**
423
     * Get the indexes from the database.
424
     *
425
     * @param string $table The name of the table to get the indexes for.
426
     * @return array|null
427
     */
428
    protected function fetchIndexesDb($table = '') {
429
        $stm = $this->get(
430
            new Identifier('information_schema', 'STATISTICS'),
431
            [
432
                'TABLE_SCHEMA' => $this->getDbName(),
433
                'TABLE_NAME' => $this->prefixTable($table, false)
434
            ],
435
            [
436
                'columns' => [
437
                    'INDEX_NAME',
438
                    'COLUMN_NAME',
439
                    'NON_UNIQUE'
440
                ],
441
                'order' => ['INDEX_NAME', 'SEQ_IN_INDEX']
442
            ]
443
        );
444
        $indexRows = $stm->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
445
446
        $indexes = [];
447
        foreach ($indexRows as $indexName => $columns) {
448
            $index = [
449
                'type' => null,
450
                'columns' => array_column($columns, 'COLUMN_NAME'),
451
                'name' => $indexName
452
            ];
453
454
            if ($indexName === 'PRIMARY') {
455
                $index['type'] = Db::INDEX_PK;
456
            } else {
457
                $index['type'] = $columns[0]['NON_UNIQUE'] ? Db::INDEX_IX : Db::INDEX_UNIQUE;
458
            }
459
            $indexes[] = $index;
460
        }
461
462
        return $indexes;
463
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468
    protected function fetchTableNamesDb() {
469
        // Get the table names.
470
        $tables = $this->get(
471
            new Identifier('information_schema', 'TABLES'),
472
            [
473
                'TABLE_SCHEMA' => $this->getDbName(),
474
                'TABLE_NAME' => [Db::OP_LIKE => $this->escapeLike($this->getPx()).'%']
475
            ],
476
            [
477
                'columns' => ['TABLE_NAME'],
478
                'fetchMode' => PDO::FETCH_ASSOC
479
            ]
480
        );
481
482
        return $tables->fetchAll(PDO::FETCH_COLUMN);
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488 11
    public function insert(string $table, array $row, array $options = []) {
489 11
        $sql = $this->buildInsert($table, $row, $options);
490 11
        $id = $this->queryID($sql, [], $options);
491 11
        if (is_numeric($id)) {
492 11
            return (int)$id;
493
        } else {
494
            return $id;
495
        }
496
    }
497
498
    /**
499
     * Build an insert statement.
500
     *
501
     * @param string|Identifier $table The name of the table to insert to.
502
     * @param array $row The row to insert.
503
     * @param array $options An array of options for the insert. See {@link Db::insert} for the options.
504
     * @return string Returns the the sql string of the insert statement.
505
     */
506
    protected function buildInsert($table, array $row, $options = []) {
507
        if (self::val(Db::OPTION_UPSERT, $options)) {
508
            return $this->buildUpsert($table, $row, $options);
509
        } elseif (self::val(Db::OPTION_IGNORE, $options)) {
510
            $sql = 'insert ignore ';
511
        } elseif (self::val(Db::OPTION_REPLACE, $options)) {
512
            $sql = 'replace ';
513
        } else {
514
            $sql = 'insert ';
515
        }
516
        $sql .= $this->prefixTable($table);
517
518
        // Add the list of values.
519
        $sql .=
520
            "\n".$this->bracketList(array_keys($row), '`').
521
            "\nvalues".$this->bracketList($row, "'");
522
523
        return $sql;
524
    }
525
526
    /**
527
     * Build an upsert statement.
528
     *
529
     * An upsert statement is an insert on duplicate key statement in MySQL.
530
     *
531
     * @param string|Identifier $table The name of the table to update.
532
     * @param array $row The row to insert or update.
533
     * @param array $options An array of additional query options.
534
     * @return string Returns the upsert statement as a string.
535
     */
536
    protected function buildUpsert($table, array $row, $options = []) {
537
        // Build the initial insert statement first.
538
        unset($options[Db::OPTION_UPSERT]);
539
        $sql = $this->buildInsert($table, $row, $options);
540
541
        // Add the duplicate key stuff.
542
        $updates = [];
543
        foreach ($row as $key => $value) {
544
            $updates[] = $this->escape($key).' = values('.$this->escape($key).')';
545
        }
546
        $sql .= "\non duplicate key update ".implode(', ', $updates);
547
548
        return $sql;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554 20
    public function load(string $table, $rows, array $options = []) {
555 20
        $count = 0;
556 20
        $first = true;
557 20
        $spec = [];
558 20
        $stmt = null;
559
560
        // Loop over the rows and insert them with the statement.
561 20
        foreach ($rows as $row) {
562 20
            if ($first) {
563
                // Build the insert statement from the first row.
564 20
                foreach ($row as $key => $value) {
565 20
                    $spec[$key] = new Literal($this->paramName($key));
566
                }
567
568 20
                $sql = $this->buildInsert($table, $spec, $options);
569 20
                $stmt = $this->getPDO()->prepare($sql);
570 20
                $first = false;
571
            }
572
573 20
            $args = array_map([$this, 'argValue'], $row);
574 20
            $stmt->execute($args);
575 20
            $count += $stmt->rowCount();
576
        }
577
578 20
        return $count;
579
    }
580
581
    /**
582
     * Make a valid PDO parameter name from a string.
583
     *
584
     * This method replaces invalid placeholder characters with underscores.
585
     *
586
     * @param string $name The name to replace.
587
     * @return string
588
     */
589 20
    protected function paramName($name) {
590 20
        $result = ':'.preg_replace('`[^a-zA-Z0-9_]`', '_', $name);
591 20
        return $result;
592
    }
593
594
    /**
595
     * {@inheritdoc}
596
     */
597 6
    public function update(string $table, array $set, array $where, array $options = []): int {
598 6
        $sql = $this->buildUpdate($table, $set, $where, $options);
599 6
        $result = $this->queryModify($sql, [], $options);
600
601 6
        return $result;
602
    }
603
604
    /**
605
     * Build a sql update statement.
606
     *
607
     * @param string|Identifier $table The name of the table to update.
608
     * @param array $set An array of columns to set.
609
     * @param array $where The where filter.
610
     * @param array $options Additional options for the query.
611
     * @return string Returns the update statement as a string.
612
     */
613
    protected function buildUpdate($table, array $set, array $where, array $options = []): string {
614
        $sql = 'update '.
615
            (self::val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
616
            $this->prefixTable($table).
617
            "\nset\n  ";
618
619
        $parts = [];
620
        foreach ($set as $key => $value) {
621
            $escapedKey = $this->escape($key);
622
623
            $parts[] = "$escapedKey = ".$this->quote($value, $escapedKey);
624
        }
625
        $sql .= implode(",\n  ", $parts);
626
627
        if (!empty($where)) {
628
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND);
629
        }
630
631
        return $sql;
632
    }
633
634
    /**
635
     * {@inheritdoc}
636
     */
637 15
    public function delete(string $table, array $where, array $options = []): int {
638 15
        if (self::val(Db::OPTION_TRUNCATE, $options)) {
639
            if (!empty($where)) {
640
                throw new \InvalidArgumentException("You cannot truncate $table with a where filter.", 500);
641
            }
642
            $sql = 'truncate table '.$this->prefixTable($table);
643
        } else {
644 15
            $sql = 'delete from '.$this->prefixTable($table);
645
646 15
            if (!empty($where)) {
647
                $sql .= "\nwhere ".$this->buildWhere($where);
648
            }
649
        }
650 15
        return $this->queryModify($sql, [], $options);
651
    }
652
653
    /**
654
     * {@inheritdoc}
655
     */
656
    protected function createTableDb(array $tableDef, array $options = []) {
657
        $table = $tableDef['name'];
658
659
        // The table doesn't exist so this is a create table.
660
        $parts = array();
661
        foreach ($tableDef['columns'] as $name => $cdef) {
662
            $parts[] = $this->columnDefString($name, $cdef);
663
        }
664
665
        foreach (self::val('indexes', $tableDef, []) as $index) {
666
            $indexDef = $this->indexDefString($table, $index);
667
            if (!empty($indexDef)) {
668
                $parts[] = $indexDef;
669
            }
670
        }
671
672
        $tableName = $this->prefixTable($table);
673
        $sql = "create table $tableName (\n  ".
674
            implode(",\n  ", $parts).
675
            "\n)";
676
677
        if (self::val('collate', $options)) {
678
            $sql .= "\n collate {$options['collate']}";
679
        }
680
681
        $this->queryDefine($sql, $options);
682
    }
683
684
    /**
685
     * Construct a column definition string from an array defintion.
686
     *
687
     * @param string $name The name of the column.
688
     * @param array $cdef The column definition.
689
     * @return string Returns a string representing the column definition.
690
     */
691
    protected function columnDefString($name, array $cdef) {
692
        $result = $this->escape($name).' '.$this->nativeDbType($cdef);
693
694
        if (!self::val('allowNull', $cdef)) {
695
            $result .= ' not null';
696
        }
697
698
        if (isset($cdef['default'])) {
699
            if (isset($cdef['default'])) {
700
                $default = $cdef['default'];
701
702
                switch ($cdef['dbtype']) {
703
                    case 'timestamp':
704
                        if (strcasecmp($default, 'current_timestamp') === 0) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
705
                            break;
706
                        }
707
                    default:
708
                        $default = $this->quote($cdef['default']);
709
                }
710
711
                $result .= " default $default";
712
            }
713
        }
714
715
        if (self::val('autoIncrement', $cdef)) {
716
            $result .= ' auto_increment';
717
        }
718
719
        return $result;
720
    }
721
722
    /**
723
     * Return the SDL string that defines an index.
724
     *
725
     * @param string $table The name of the table that the index is on.
726
     * @param array $def The index definition. This definition should have the following keys.
727
     *
728
     * columns
729
     * : An array of columns in the index.
730
     * type
731
     * : One of "index", "unique", or "primary".
732
     * @return null|string Returns the index string or null if the index is not correct.
733
     */
734
    protected function indexDefString($table, array $def) {
735
        $indexName = $this->escape($this->buildIndexName($table, $def));
736
        if (empty($def['columns'])) {
737
            throw new \DomainException("The `$table`.$indexName index has no columns.", 500);
738
        }
739
        switch (self::val('type', $def, Db::INDEX_IX)) {
740
            case Db::INDEX_IX:
741
                return "index $indexName ".$this->bracketList($def['columns'], '`');
742
            case Db::INDEX_UNIQUE:
743
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
744
            case Db::INDEX_PK:
745
                return "primary key ".$this->bracketList($def['columns'], '`');
746
        }
747
        return null;
748
    }
749
750
    /**
751
     * {@inheritdoc}
752
     */
753
    protected function alterTableDb(array $alterDef, array $options = []) {
754
        $table = $alterDef['name'];
755
        $columnOrders = $this->getColumnOrders($alterDef['def']['columns']);
756
        $parts = [];
757
758
        // Add the columns and indexes.
759
        foreach ($alterDef['add']['columns'] as $cname => $cdef) {
760
            // Figure out the order of the column.
761
            $pos = self::val($cname, $columnOrders, '');
762
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
763
        }
764
        foreach ($alterDef['add']['indexes'] as $ixdef) {
765
            $parts[] = 'add '.$this->indexDefString($table, $ixdef);
766
        }
767
768
        // Alter the columns.
769
        foreach ($alterDef['alter']['columns'] as $cname => $cdef) {
770
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
771
        }
772
773
        // Drop the columns and indexes.
774
        foreach ($alterDef['drop']['columns'] as $cname => $_) {
775
            $parts[] = 'drop '.$this->escape($cname);
776
        }
777
        foreach ($alterDef['drop']['indexes'] as $ixdef) {
778
            $parts[] = 'drop index '.$this->escape($ixdef['name']);
779
        }
780
781
        if (empty($parts)) {
782
            return false;
783
        }
784
785
        $sql = 'alter '.
786
            (self::val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
787
            'table '.$this->prefixTable($table)."\n  ".
788
            implode(",\n  ", $parts);
789
790
        $this->queryDefine($sql, $options);
791
    }
792
793
    /**
794
     * Get an array of column orders so that added columns can be slotted into their correct spot.
795
     *
796
     * @param array $cdefs An array of column definitions.
797
     * @return array Returns an array of column orders suitable for an `alter table` statement.
798
     */
799
    private function getColumnOrders($cdefs) {
800
        $orders = array_flip(array_keys($cdefs));
801
802
        $prev = ' first';
803
        foreach ($orders as $cname => &$value) {
804
            $value = $prev;
805
            $prev = ' after '.$this->escape($cname);
806
        }
807
        return $orders;
808
    }
809
810
    /**
811
     * Force a value into the appropriate php type based on its SQL type.
812
     *
813
     * @param mixed $value The value to force.
814
     * @param string $type The sqlite type name.
815
     * @return mixed Returns $value cast to the appropriate type.
816
     */
817
    protected function forceType($value, $type) {
818
        $type = strtolower($type);
819
820
        if ($type === 'null') {
821
            return null;
822
        } elseif ($type === 'boolean') {
823
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
824
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
825
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
826
            return filter_var($value, FILTER_VALIDATE_INT);
827
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
828
            'numeric', 'number', 'decimal(10,5)'])) {
829
            return filter_var($value, FILTER_VALIDATE_FLOAT);
830
        } else {
831
            return (string)$value;
832
        }
833
    }
834
835
    public function quote($value, string $column = ''): string {
836
        if (is_bool($value)) {
837
            return (string)(int)$value;
838
        } elseif ($value instanceof \DateTimeInterface) {
839
            $value = $value->format(self::MYSQL_DATE_FORMAT);
840
        } elseif ($value === null) {
841
            return 'null';
842
        }
843
844
        return parent::quote($value, $column);
845
    }
846
847
    /**
848
     * Convert a value into something usable as a PDO parameter.
849
     *
850
     * @param mixed $value The value to convert.
851
     * @return mixed Returns the converted value or the value itself if it's fine.
852
     */
853 20
    private function argValue($value) {
854 20
        if (is_bool($value)) {
855
            return (int)$value;
856 20
        } elseif ($value instanceof \DateTimeInterface) {
857
            return $value->format(self::MYSQL_DATE_FORMAT);
858
        } else {
859 20
            return $value;
860
        }
861
    }
862
}
863