MySqlDb::buildSelect()   B
last analyzed

Complexity

Conditions 11
Paths 96

Size

Total Lines 58
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 11.0908

Importance

Changes 0
Metric Value
cc 11
eloc 35
nc 96
nop 3
dl 0
loc 58
ccs 30
cts 33
cp 0.9091
crap 11.0908
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 10
    protected function dropTableDb(string $table, array $options = []) {
40
        $sql = 'drop table '.
41 10
            ($options[Db::OPTION_IGNORE] ? 'if exists ' : '').
42 10
            $this->prefixTable($table);
43
44 10
        $this->queryDefine($sql);
45 10
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 14
    protected function fetchTableDefDb(string $table) {
51 14
        $columns = $this->fetchColumnDefsDb($table);
52
53 14
        if (empty($columns)) {
54
            // A table with no columns does not exist.
55 10
            return null;
56
        }
57
58 12
        $indexes = $this->fetchIndexesDb($table);
59
60
        $tableDef = [
61 12
            'name' => $table,
62 12
            'columns' => $columns,
63 12
            'indexes' => $indexes
64
        ];
65
66 12
        return $tableDef;
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 9
    protected function fetchColumnDefsDb(string $table) {
73 9
        $rows = $this->get(
74 9
            new Identifier('information_schema', 'COLUMNS'),
75
            [
76 9
                'TABLE_SCHEMA' => $this->getDbName(),
77 9
                'TABLE_NAME' => $this->prefixTable($table, false)
78
            ],
79
            [
80 9
                Db::OPTION_FETCH_MODE => PDO::FETCH_ASSOC,
81
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
82
            ]
83
        );
84
85 9
        $columns = [];
86 9
        foreach ($rows as $row) {
87 8
            $columnType = $row['COLUMN_TYPE'];
88 8
            if ($columnType === 'tinyint(1)') {
89 1
                $columnType = 'bool';
90
            }
91 8
            $column = Db::typeDef($columnType);
92 8
            if ($column === null) {
93
                throw new \Exception("Unknown type '$columnType'.", 500);
94
            }
95
96 8
            $column['allowNull'] = strcasecmp($row['IS_NULLABLE'], 'YES') === 0;
97
98 8
            if (($default = $row['COLUMN_DEFAULT']) !== null) {
99 4
                $column['default'] = $this->forceType($default, $column['type']);
100
            }
101
102 8
            if ($row['EXTRA'] === 'auto_increment') {
103 1
                $column['autoIncrement'] = true;
104
            }
105
106 8
            if ($row['COLUMN_KEY'] === 'PRI') {
107 3
                $column['primary'] = true;
108
            }
109
110 8
            $columns[$row['COLUMN_NAME']] = $column;
111
        }
112
113 9
        return $columns;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 90
    public function get($table, array $where, array $options = []): \PDOStatement {
120 90
        $sql = $this->buildSelect($table, $where, $options);
121 90
        $result = $this->query($sql, [], $options);
122 90
        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 93
    protected function buildSelect($table, array $where, array $options = []) {
135 93
        $options += ['limit' => 0];
136
137 93
        $sql = '';
138
139
        // Build the select clause.
140 93
        if (!empty($options['columns'])) {
141 25
            $columns = array();
142 25
            foreach ($options['columns'] as $value) {
143 25
                $columns[] = $this->escape($value);
144
            }
145 25
            $sql .= 'select '.implode(', ', $columns);
146
        } else {
147 80
            $sql .= "select *";
148
        }
149
150
        // Build the from clause.
151 93
        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 93
            $table = $this->prefixTable($table);
155
        }
156 93
        $sql .= "\nfrom $table";
157
158
        // Build the where clause.
159 93
        $whereString = $this->buildWhere($where, Db::OP_AND);
160 93
        if ($whereString) {
161 61
            $sql .= "\nwhere ".$whereString;
162
        }
163
164
        // Build the order.
165 93
        if (!empty($options['order'])) {
166 44
            $orders = [];
167 44
            foreach ($options['order'] as $column) {
168 44
                if ($column[0] === '-') {
169 2
                    $order = $this->escape(substr($column, 1)).' desc';
170
                } else {
171 44
                    $order = $this->escape($column);
172
                }
173 44
                $orders[] = $order;
174
            }
175 44
            $sql .= "\norder by ".implode(', ', $orders);
176
        }
177
178
        // Build the limit, offset.
179 93
        if (!empty($options['limit'])) {
180 16
            $limit = (int)$options['limit'];
181 16
            $sql .= "\nlimit $limit";
182
        }
183
184 93
        if (!empty($options['offset'])) {
185 1
            $sql .= ' offset '.((int)$options['offset']);
186 93
        } elseif (isset($options['page'])) {
187
            $offset = $options['limit'] * ($options['page'] - 1);
188
            $sql .= ' offset '.$offset;
189
        }
190
191 93
        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 97
    protected function buildWhere($where, $op = Db::OP_AND) {
203 97
        $map = static::$map;
204 97
        $strop = $map[$op];
205
206 97
        $result = '';
207 97
        foreach ($where as $column => $value) {
208 65
            $btcolumn = $this->escape($column);
209
210 65
            if (is_array($value)) {
211 42
                if (is_numeric($column)) {
212
                    // This is a bracketed expression.
213 4
                    $result .= (empty($result) ? '' : "\n  $strop ").
214 4
                        "(\n  ".
215 4
                        $this->buildWhere($value, $op).
216 4
                        "\n  )";
217 42
                } elseif (in_array($column, [Db::OP_AND, Db::OP_OR])) {
218
                    // This is an AND/OR expression.
219 4
                    $result .= (empty($result) ? '' : "\n  $strop ").
220 4
                        "(\n  ".
221 4
                        $this->buildWhere($value, $column).
222 4
                        "\n  )";
223
                } else {
224 38
                    if (isset($value[0])) {
225
                        // This is a short in syntax.
226 2
                        $value = [Db::OP_IN => $value];
227
                    }
228
229 42
                    foreach ($value as $vop => $rval) {
230 38
                        if ($result) {
231 16
                            $result .= "\n  $strop ";
232
                        }
233
234 38
                        switch ($vop) {
235
                            case Db::OP_AND:
236
                            case Db::OP_OR:
237 4
                                if (is_numeric($column)) {
238
                                    $innerWhere = $rval;
239
                                } else {
240 4
                                    $innerWhere = [$column => $rval];
241
                                }
242
                                $result .= "(\n  ".
243 4
                                    $this->buildWhere($innerWhere, $vop).
244 4
                                    "\n  )";
245 4
                                break;
246
                            case Db::OP_EQ:
247 6
                                if ($rval === null) {
248
                                    $result .= "$btcolumn is null";
249 6
                                } elseif (is_array($rval)) {
250 2
                                    $result .= "$btcolumn in ".$this->bracketList($rval);
251
                                } else {
252 4
                                    $result .= "$btcolumn = ".$this->quote($rval);
253
                                }
254 6
                                break;
255
                            case Db::OP_GT:
256
                            case Db::OP_GTE:
257
                            case Db::OP_LT:
258
                            case Db::OP_LTE:
259 12
                                $result .= "$btcolumn {$map[$vop]} ".$this->quote($rval);
260 12
                                break;
261
                            case Db::OP_LIKE:
262 12
                                $result .= $this->buildLike($btcolumn, $rval);
263 12
                                break;
264
                            case Db::OP_IN:
265
                                // Quote the in values.
266 4
                                $rval = array_map([$this, 'quote'], (array)$rval);
267 4
                                $result .= "$btcolumn in (".implode(', ', $rval).')';
268 4
                                break;
269
                            case Db::OP_NEQ:
270 6
                                if ($rval === null) {
271 2
                                    $result .= "$btcolumn is not null";
272 4
                                } elseif (is_array($rval)) {
273 2
                                    $result .= "$btcolumn not in ".$this->bracketList($rval);
274
                                } else {
275 2
                                    $result .= "$btcolumn <> ".$this->quote($rval);
276
                                }
277 6
                                break;
278
                        }
279
                    }
280
                }
281
            } else {
282 39
                if ($result) {
283 13
                    $result .= "\n  $strop ";
284
                }
285
286
                // This is just an equality operator.
287 39
                if ($value === null) {
288 2
                    $result .= "$btcolumn is null";
289
                } else {
290 37
                    $result .= "$btcolumn = ".$this->quote($value);
291
                }
292
            }
293
        }
294 97
        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 6
    protected function buildLike(string $column, $value): string {
305 6
        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 84
    public function bracketList($row, $quote = "'") {
320 84
        switch ($quote) {
321 84
            case "'":
322 67
                $row = array_map([$this, 'quote'], $row);
323 67
                $quote = '';
324 67
                break;
325 84
            case '`':
326 84
                $row = array_map([$this, 'escape'], $row);
327 84
                $quote = '';
328 84
                break;
329
        }
330
331 84
        return "($quote".implode("$quote, $quote", $row)."$quote)";
332
    }
333
334
335
    /**
336
     * Get the current database name.
337
     *
338
     * @return mixed
339
     */
340 9
    private function getDbName() {
341 9
        if (!isset($this->dbname)) {
342
            $this->dbname = $this->getPDO()->query('select database()')->fetchColumn();
343
        }
344 9
        return $this->dbname;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350 25
    protected function nativeDbType(array $type) {
351 25
        static $translations = ['bool' => 'tinyint(1)', 'byte' => 'tinyint', 'short' => 'smallint', 'long' => 'bigint'];
352
353
        // Translate the dbtype to a MySQL native type.
354 25
        if (isset($translations[$type['dbtype']])) {
355 1
            $type['dbtype'] = $translations[$type['dbtype']];
356
        }
357
358
        // Unsigned is represented differently in MySQL.
359 25
        $unsigned = !empty($type['unsigned']);
360 25
        unset($type['unsigned']);
361
362
        // The max length is not specified for text columns.
363 25
        if (in_array($type['dbtype'], ['tinytext', 'text', 'mediumtext', 'longtext'])) {
364 2
            unset($type['maxLength']);
365
        }
366
367 25
        $dbType = static::dbType($type).($unsigned ? ' unsigned' : '');
368
369 25
        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 6
    protected function fetchIndexesDb($table = '') {
429 6
        $stm = $this->get(
430 6
            new Identifier('information_schema', 'STATISTICS'),
431
            [
432 6
                'TABLE_SCHEMA' => $this->getDbName(),
433 6
                'TABLE_NAME' => $this->prefixTable($table, false)
434
            ],
435
            [
436 6
                'columns' => [
437
                    'INDEX_NAME',
438
                    'COLUMN_NAME',
439
                    'NON_UNIQUE'
440
                ],
441
                'order' => ['INDEX_NAME', 'SEQ_IN_INDEX']
442
            ]
443
        );
444 6
        $indexRows = $stm->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
445
446 6
        $indexes = [];
447 6
        foreach ($indexRows as $indexName => $columns) {
448
            $index = [
449 4
                'type' => null,
450 4
                'columns' => array_column($columns, 'COLUMN_NAME'),
451 4
                'name' => $indexName
452
            ];
453
454 4
            if ($indexName === 'PRIMARY') {
455 2
                $index['type'] = Db::INDEX_PK;
456
            } else {
457 2
                $index['type'] = $columns[0]['NON_UNIQUE'] ? Db::INDEX_IX : Db::INDEX_UNIQUE;
458
            }
459 4
            $indexes[] = $index;
460
        }
461
462 6
        return $indexes;
463
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468 6
    protected function fetchTableNamesDb() {
469
        // Get the table names.
470 6
        $tables = $this->get(
471 6
            new Identifier('information_schema', 'TABLES'),
472
            [
473 6
                'TABLE_SCHEMA' => $this->getDbName(),
474 6
                'TABLE_NAME' => [Db::OP_LIKE => $this->escapeLike($this->getPx()).'%']
475
            ],
476
            [
477 6
                'columns' => ['TABLE_NAME'],
478
                'fetchMode' => PDO::FETCH_ASSOC
479
            ]
480
        );
481
482 6
        return $tables->fetchAll(PDO::FETCH_COLUMN);
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488 23
    public function insert(string $table, array $row, array $options = []) {
489 23
        $sql = $this->buildInsert($table, $row, $options);
490 23
        $id = $this->queryID($sql, [], $options);
491 23
        if (is_numeric($id)) {
492 23
            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 36
    protected function buildInsert($table, array $row, $options = []) {
507 36
        if (self::val(Db::OPTION_UPSERT, $options)) {
508 2
            return $this->buildUpsert($table, $row, $options);
509 36
        } elseif (self::val(Db::OPTION_IGNORE, $options)) {
510 2
            $sql = 'insert ignore ';
511 35
        } elseif (self::val(Db::OPTION_REPLACE, $options)) {
512 2
            $sql = 'replace ';
513
        } else {
514 34
            $sql = 'insert ';
515
        }
516 36
        $sql .= $this->prefixTable($table);
517
518
        // Add the list of values.
519
        $sql .=
520 36
            "\n".$this->bracketList(array_keys($row), '`').
521 36
            "\nvalues".$this->bracketList($row, "'");
522
523 36
        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 2
    protected function buildUpsert($table, array $row, $options = []) {
537
        // Build the initial insert statement first.
538 2
        unset($options[Db::OPTION_UPSERT]);
539 2
        $sql = $this->buildInsert($table, $row, $options);
540
541
        // Add the duplicate key stuff.
542 2
        $updates = [];
543 2
        foreach ($row as $key => $value) {
544 2
            $updates[] = $this->escape($key).' = values('.$this->escape($key).')';
545
        }
546 2
        $sql .= "\non duplicate key update ".implode(', ', $updates);
547
548 2
        return $sql;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554 54
    public function load(string $table, $rows, array $options = []) {
555 54
        $count = 0;
556 54
        $first = true;
557 54
        $spec = [];
558 54
        $stmt = null;
559
560
        // Loop over the rows and insert them with the statement.
561 54
        foreach ($rows as $row) {
562 44
            if ($first) {
563
                // Build the insert statement from the first row.
564 44
                foreach ($row as $key => $value) {
565 44
                    $spec[$key] = new Literal($this->paramName($key));
566
                }
567
568 44
                $sql = $this->buildInsert($table, $spec, $options);
569 44
                $stmt = $this->getPDO()->prepare($sql);
570 44
                $first = false;
571
            }
572
573 44
            $args = array_map([$this, 'argValue'], $row);
574 44
            $stmt->execute($args);
575 44
            $count += $stmt->rowCount();
576
        }
577
578 54
        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 44
    protected function paramName($name) {
590 44
        $result = ':'.preg_replace('`[^a-zA-Z0-9_]`', '_', $name);
591 44
        return $result;
592
    }
593
594
    /**
595
     * {@inheritdoc}
596
     */
597 10
    public function update(string $table, array $set, array $where, array $options = []): int {
598 10
        $sql = $this->buildUpdate($table, $set, $where, $options);
599 10
        $result = $this->queryModify($sql, [], $options);
600
601 10
        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 4
    protected function buildUpdate($table, array $set, array $where, array $options = []): string {
614
        $sql = 'update '.
615 4
            (self::val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
616 4
            $this->prefixTable($table).
617 4
            "\nset\n  ";
618
619 4
        $parts = [];
620 4
        foreach ($set as $key => $value) {
621 4
            $escapedKey = $this->escape($key);
622
623 4
            $parts[] = "$escapedKey = ".$this->quote($value, $escapedKey);
624
        }
625 4
        $sql .= implode(",\n  ", $parts);
626
627 4
        if (!empty($where)) {
628 4
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND);
629
        }
630
631 4
        return $sql;
632
    }
633
634
    /**
635
     * {@inheritdoc}
636
     */
637 30
    public function delete(string $table, array $where, array $options = []): int {
638 30
        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 30
            $sql = 'delete from '.$this->prefixTable($table);
645
646 30
            if (!empty($where)) {
647
                $sql .= "\nwhere ".$this->buildWhere($where);
648
            }
649
        }
650 30
        return $this->queryModify($sql, [], $options);
651
    }
652
653
    /**
654
     * {@inheritdoc}
655
     */
656 25
    protected function createTableDb(array $tableDef, array $options = []) {
657 25
        $table = $tableDef['name'];
658
659
        // The table doesn't exist so this is a create table.
660 25
        $parts = array();
661 25
        foreach ($tableDef['columns'] as $name => $cdef) {
662 25
            $parts[] = $this->columnDefString($name, $cdef);
663
        }
664
665 25
        foreach (self::val('indexes', $tableDef, []) as $index) {
666 19
            $indexDef = $this->indexDefString($table, $index);
667 19
            if (!empty($indexDef)) {
668 18
                $parts[] = $indexDef;
669
            }
670
        }
671
672 25
        $tableName = $this->prefixTable($table);
673 25
        $sql = "create table $tableName (\n  ".
674 25
            implode(",\n  ", $parts).
675 25
            "\n)";
676
677 25
        if (self::val('collate', $options)) {
678
            $sql .= "\n collate {$options['collate']}";
679
        }
680
681 25
        $this->queryDefine($sql, $options);
682 25
    }
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 25
    protected function columnDefString($name, array $cdef) {
692 25
        $result = $this->escape($name).' '.$this->nativeDbType($cdef);
693
694 25
        if (!self::val('allowNull', $cdef)) {
695 23
            $result .= ' not null';
696
        }
697
698 25
        if (isset($cdef['default'])) {
699 15
            if (isset($cdef['default'])) {
700 15
                $default = $cdef['default'];
701
702 15
                switch ($cdef['dbtype']) {
703 15
                    case 'timestamp':
704 1
                        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 1
                            break;
706
                        }
707
                    default:
708 14
                        $default = $this->quote($cdef['default']);
709
                }
710
711 15
                $result .= " default $default";
712
            }
713
        }
714
715 25
        if (self::val('autoIncrement', $cdef)) {
716 11
            $result .= ' auto_increment';
717
        }
718
719 25
        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 19
    protected function indexDefString($table, array $def) {
735 19
        $indexName = $this->escape($this->buildIndexName($table, $def));
736 19
        if (empty($def['columns'])) {
737
            throw new \DomainException("The `$table`.$indexName index has no columns.", 500);
738
        }
739 19
        switch (self::val('type', $def, Db::INDEX_IX)) {
740
            case Db::INDEX_IX:
741 11
                return "index $indexName ".$this->bracketList($def['columns'], '`');
742
            case Db::INDEX_UNIQUE:
743 1
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
744
            case Db::INDEX_PK:
745 16
                return "primary key ".$this->bracketList($def['columns'], '`');
746
        }
747 2
        return null;
748
    }
749
750
    /**
751
     * {@inheritdoc}
752
     */
753 8
    protected function alterTableDb(array $alterDef, array $options = []) {
754 8
        $table = $alterDef['name'];
755 8
        $columnOrders = $this->getColumnOrders($alterDef['def']['columns']);
756 8
        $parts = [];
757
758
        // Add the columns and indexes.
759 8
        foreach ($alterDef['add']['columns'] as $cname => $cdef) {
760
            // Figure out the order of the column.
761 3
            $pos = self::val($cname, $columnOrders, '');
762 3
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
763
        }
764 8
        foreach ($alterDef['add']['indexes'] as $ixdef) {
765 3
            $parts[] = 'add '.$this->indexDefString($table, $ixdef);
766
        }
767
768
        // Alter the columns.
769 8
        foreach ($alterDef['alter']['columns'] as $cname => $cdef) {
770 5
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
771
        }
772
773
        // Drop the columns and indexes.
774 8
        foreach ($alterDef['drop']['columns'] as $cname => $_) {
775
            $parts[] = 'drop '.$this->escape($cname);
776
        }
777 8
        foreach ($alterDef['drop']['indexes'] as $ixdef) {
778 2
            $parts[] = 'drop index '.$this->escape($ixdef['name']);
779
        }
780
781 8
        if (empty($parts)) {
782
            return false;
783
        }
784
785
        $sql = 'alter '.
786 8
            (self::val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
787 8
            'table '.$this->prefixTable($table)."\n  ".
788 8
            implode(",\n  ", $parts);
789
790 8
        $this->queryDefine($sql, $options);
791 8
    }
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 8
    private function getColumnOrders($cdefs) {
800 8
        $orders = array_flip(array_keys($cdefs));
801
802 8
        $prev = ' first';
803 8
        foreach ($orders as $cname => &$value) {
804 8
            $value = $prev;
805 8
            $prev = ' after '.$this->escape($cname);
806
        }
807 8
        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 4
    protected function forceType($value, $type) {
818 4
        $type = strtolower($type);
819
820 4
        if ($type === 'null') {
821
            return null;
822 4
        } elseif ($type === 'boolean') {
823 1
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
824 3
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
825
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
826 3
            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 47
    public function quote($value, string $column = ''): string {
836 47
        if (is_bool($value)) {
837 1
            return (string)(int)$value;
838 47
        } elseif ($value instanceof \DateTimeInterface) {
839 2
            $value = $value->format(self::MYSQL_DATE_FORMAT);
840 47
        } elseif ($value === null) {
841 2
            return 'null';
842
        }
843
844 46
        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 44
    private function argValue($value) {
854 44
        if (is_bool($value)) {
855
            return (int)$value;
856 44
        } elseif ($value instanceof \DateTimeInterface) {
857
            return $value->format(self::MYSQL_DATE_FORMAT);
858
        } else {
859 44
            return $value;
860
        }
861
    }
862
}
863