Completed
Push — master ( c5d03b...495299 )
by Todd
12s
created

MySqlDb::argValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.3332

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 6
cp 0.6667
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.3332
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 12
    protected function fetchTableDefDb(string $table) {
51 12
        $columns = $this->fetchColumnDefsDb($table);
52
53 12
        if (empty($columns)) {
54
            // A table with no columns does not exist.
55 8
            return null;
56
        }
57
58 10
        $indexes = $this->fetchIndexesDb($table);
59
60
        $tableDef = [
61 10
            'name' => $table,
62 10
            'columns' => $columns,
63 10
            'indexes' => $indexes
64
        ];
65
66 10
        return $tableDef;
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 7
    protected function fetchColumnDefsDb(string $table) {
73 7
        $rows = $this->get(
74 7
            new Identifier('information_schema', 'COLUMNS'),
75
            [
76 7
                'TABLE_SCHEMA' => $this->getDbName(),
77 7
                'TABLE_NAME' => $this->prefixTable($table, false)
78
            ],
79
            [
80 7
                Db::OPTION_FETCH_MODE => PDO::FETCH_ASSOC,
81
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
82
            ]
83
        );
84
85 7
        $columns = [];
86 7
        foreach ($rows as $row) {
87 6
            $columnType = $row['COLUMN_TYPE'];
88 6
            if ($columnType === 'tinyint(1)') {
89 1
                $columnType = 'bool';
90
            }
91 6
            $column = Db::typeDef($columnType);
92 6
            if ($column === null) {
93
                throw new \Exception("Unknown type '$columnType'.", 500);
94
            }
95
96 6
            $column['allowNull'] = strcasecmp($row['IS_NULLABLE'], 'YES') === 0;
97
98 6
            if (($default = $row['COLUMN_DEFAULT']) !== null) {
99 4
                $column['default'] = $this->forceType($default, $column['type']);
100
            }
101
102 6
            if ($row['EXTRA'] === 'auto_increment') {
103 1
                $column['autoIncrement'] = true;
104
            }
105
106 6
            if ($row['COLUMN_KEY'] === 'PRI') {
107 3
                $column['primary'] = true;
108
            }
109
110 6
            $columns[$row['COLUMN_NAME']] = $column;
111
        }
112
113 7
        return $columns;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 79
    public function get($table, array $where, array $options = []): \PDOStatement {
120 79
        $sql = $this->buildSelect($table, $where, $options);
121 79
        $result = $this->query($sql, [], $options);
122 79
        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 85
    protected function buildSelect($table, array $where, array $options = []) {
135 85
        $options += ['limit' => 0];
136
137 85
        $sql = '';
138
139
        // Build the select clause.
140 85
        if (!empty($options['columns'])) {
141 17
            $columns = array();
142 17
            foreach ($options['columns'] as $value) {
143 17
                $columns[] = $this->escape($value);
144
            }
145 17
            $sql .= 'select '.implode(', ', $columns);
146
        } else {
147 78
            $sql .= "select *";
148
        }
149
150
        // Build the from clause.
151 85
        if ($table instanceof Literal) {
152
            $table = $table->getValue($this);
153
        } else {
154 85
            $table = $this->prefixTable($table);
155
        }
156 85
        $sql .= "\nfrom $table";
157
158
        // Build the where clause.
159 85
        $whereString = $this->buildWhere($where, Db::OP_AND);
160 85
        if ($whereString) {
161 54
            $sql .= "\nwhere ".$whereString;
162
        }
163
164
        // Build the order.
165 85
        if (!empty($options['order'])) {
166 42
            $orders = [];
167 42
            foreach ($options['order'] as $column) {
168 42
                if ($column[0] === '-') {
169 2
                    $order = $this->escape(substr($column, 1)).' desc';
170
                } else {
171 42
                    $order = $this->escape($column);
172
                }
173 42
                $orders[] = $order;
174
            }
175 42
            $sql .= "\norder by ".implode(', ', $orders);
176
        }
177
178
        // Build the limit, offset.
179 85
        if (!empty($options['limit'])) {
180 16
            $limit = (int)$options['limit'];
181 16
            $sql .= "\nlimit $limit";
182
        }
183
184 85
        if (!empty($options['offset'])) {
185 1
            $sql .= ' offset '.((int)$options['offset']);
186 85
        } elseif (isset($options['page'])) {
187
            $offset = $options['limit'] * ($options['page'] - 1);
188
            $sql .= ' offset '.$offset;
189
        }
190
191 85
        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 89
    protected function buildWhere($where, $op = Db::OP_AND) {
203 89
        $map = static::$map;
204 89
        $strop = $map[$op];
205
206 89
        $result = '';
207 89
        foreach ($where as $column => $value) {
208 58
            $btcolumn = $this->escape($column);
209
210 58
            if (is_array($value)) {
211 32
                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 32
                } 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 28
                    if (isset($value[0])) {
225
                        // This is a short in syntax.
226 2
                        $value = [Db::OP_IN => $value];
227
                    }
228
229 28
                    foreach ($value as $vop => $rval) {
230 28
                        if ($result) {
231 6
                            $result .= "\n  $strop ";
232
                        }
233
234
                        switch ($vop) {
235 28
                            case Db::OP_AND:
236 28
                            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).
0 ignored issues
show
Bug introduced by
It seems like $innerWhere defined by $rval on line 238 can also be of type null; however, Garden\Db\Drivers\MySqlDb::buildWhere() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
244 4
                                    "\n  )";
245 4
                                break;
246 28
                            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 24
                            case Db::OP_GT:
256 22
                            case Db::OP_GTE:
257 20
                            case Db::OP_LT:
258 14
                            case Db::OP_LTE:
259 12
                                $result .= "$btcolumn {$map[$vop]} ".$this->quote($rval);
260 12
                                break;
261 12
                            case Db::OP_LIKE:
262 2
                                $result .= $this->buildLike($btcolumn, $rval);
263 2
                                break;
264 10
                            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 6
                            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 32
                                break;
278
                        }
279
                    }
280
                }
281
            } else {
282 32
                if ($result) {
283 11
                    $result .= "\n  $strop ";
284
                }
285
286
                // This is just an equality operator.
287 32
                if ($value === null) {
288 2
                    $result .= "$btcolumn is null";
289
                } else {
290 58
                    $result .= "$btcolumn = ".$this->quote($value);
291
                }
292
            }
293
        }
294 89
        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 1
    protected function buildLike(string $column, $value): string {
305 1
        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 83
    public function bracketList($row, $quote = "'") {
320
        switch ($quote) {
321 83
            case "'":
322 67
                $row = array_map([$this, 'quote'], $row);
323 67
                $quote = '';
324 67
                break;
325 83
            case '`':
326 83
                $row = array_map([$this, 'escape'], $row);
327 83
                $quote = '';
328 83
                break;
329
        }
330
331 83
        return "($quote".implode("$quote, $quote", $row)."$quote)";
332
    }
333
334
335
    /**
336
     * Get the current database name.
337
     *
338
     * @return mixed
339
     */
340 7
    private function getDbName() {
341 7
        if (!isset($this->dbname)) {
342
            $this->dbname = $this->getPDO()->query('select database()')->fetchColumn();
343
        }
344 7
        return $this->dbname;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350 22
    protected function nativeDbType(array $type) {
351 22
        static $translations = ['bool' => 'tinyint(1)', 'byte' => 'tinyint', 'short' => 'smallint', 'long' => 'bigint'];
352
353
        // Translate the dbtype to a MySQL native type.
354 22
        if (isset($translations[$type['dbtype']])) {
355 1
            $type['dbtype'] = $translations[$type['dbtype']];
356
        }
357
358
        // Unsigned is represented differently in MySQL.
359 22
        $unsigned = !empty($type['unsigned']);
360 22
        unset($type['unsigned']);
361
362
        // The max length is not specified for text columns.
363 22
        if (in_array($type['dbtype'], ['tinytext', 'text', 'mediumtext', 'longtext'])) {
364 2
            unset($type['maxLength']);
365
        }
366
367 22
        $dbType = static::dbType($type).($unsigned ? ' unsigned' : '');
368
369 22
        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.
0 ignored issues
show
Documentation introduced by
Should the return type not be string[]|null|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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 5
    protected function fetchIndexesDb($table = '') {
429 5
        $stm = $this->get(
430 5
            new Identifier('information_schema', 'STATISTICS'),
431
            [
432 5
                'TABLE_SCHEMA' => $this->getDbName(),
433 5
                'TABLE_NAME' => $this->prefixTable($table, false)
434
            ],
435
            [
436 5
                'columns' => [
437
                    'INDEX_NAME',
438
                    'COLUMN_NAME',
439
                    'NON_UNIQUE'
440
                ],
441
                'order' => ['INDEX_NAME', 'SEQ_IN_INDEX']
442
            ]
443
        );
444 5
        $indexRows = $stm->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
445
446 5
        $indexes = [];
447 5
        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 5
        return $indexes;
463
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468 1
    protected function fetchTableNamesDb() {
469
        // Get the table names.
470 1
        $tables = $this->get(
471 1
            new Identifier('information_schema', 'TABLES'),
472
            [
473 1
                'TABLE_SCHEMA' => $this->getDbName(),
474 1
                'TABLE_NAME' => [Db::OP_LIKE => $this->escapeLike($this->getPx()).'%']
475
            ],
476
            [
477 1
                'columns' => ['TABLE_NAME'],
478
                'fetchMode' => PDO::FETCH_ASSOC
479
            ]
480
        );
481
482 1
        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 22
    protected function createTableDb(array $tableDef, array $options = []) {
657 22
        $table = $tableDef['name'];
658
659
        // The table doesn't exist so this is a create table.
660 22
        $parts = array();
661 22
        foreach ($tableDef['columns'] as $name => $cdef) {
662 22
            $parts[] = $this->columnDefString($name, $cdef);
663
        }
664
665 22
        foreach (self::val('indexes', $tableDef, []) as $index) {
666 19
            $indexDef = $this->indexDefString($table, $index);
667 19
            if (!empty($indexDef)) {
668 19
                $parts[] = $indexDef;
669
            }
670
        }
671
672 22
        $tableName = $this->prefixTable($table);
673 22
        $sql = "create table $tableName (\n  ".
674 22
            implode(",\n  ", $parts).
675 22
            "\n)";
676
677 22
        if (self::val('collate', $options)) {
678
            $sql .= "\n collate {$options['collate']}";
679
        }
680
681 22
        $this->queryDefine($sql, $options);
682 22
    }
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 22
    protected function columnDefString($name, array $cdef) {
692 22
        $result = $this->escape($name).' '.$this->nativeDbType($cdef);
693
694 22
        if (!self::val('allowNull', $cdef)) {
695 20
            $result .= ' not null';
696
        }
697
698 22
        if (isset($cdef['default'])) {
699 14
            $result .= ' default '.$this->quote($cdef['default']);
700
        }
701
702 22
        if (self::val('autoIncrement', $cdef)) {
703 11
            $result .= ' auto_increment';
704
        }
705
706 22
        return $result;
707
    }
708
709
    /**
710
     * Return the SDL string that defines an index.
711
     *
712
     * @param string $table The name of the table that the index is on.
713
     * @param array $def The index definition. This definition should have the following keys.
714
     *
715
     * columns
716
     * : An array of columns in the index.
717
     * type
718
     * : One of "index", "unique", or "primary".
719
     * @return null|string Returns the index string or null if the index is not correct.
720
     */
721 19
    protected function indexDefString($table, array $def) {
722 19
        $indexName = $this->escape($this->buildIndexName($table, $def));
723 19
        if (empty($def['columns'])) {
724
            throw new \DomainException("The `$table`.$indexName index has no columns.", 500);
725
        }
726 19
        switch (self::val('type', $def, Db::INDEX_IX)) {
727 19
            case Db::INDEX_IX:
728 11
                return "index $indexName ".$this->bracketList($def['columns'], '`');
729 17
            case Db::INDEX_UNIQUE:
730 1
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
731 17
            case Db::INDEX_PK:
732 16
                return "primary key ".$this->bracketList($def['columns'], '`');
733
        }
734 2
        return null;
735
    }
736
737
    /**
738
     * {@inheritdoc}
739
     */
740 7
    protected function alterTableDb(array $alterDef, array $options = []) {
741 7
        $table = $alterDef['name'];
742 7
        $columnOrders = $this->getColumnOrders($alterDef['def']['columns']);
743 7
        $parts = [];
744
745
        // Add the columns and indexes.
746 7
        foreach ($alterDef['add']['columns'] as $cname => $cdef) {
747
            // Figure out the order of the column.
748 3
            $pos = self::val($cname, $columnOrders, '');
749 3
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
750
        }
751 7
        foreach ($alterDef['add']['indexes'] as $ixdef) {
752 3
            $parts[] = 'add '.$this->indexDefString($table, $ixdef);
753
        }
754
755
        // Alter the columns.
756 7
        foreach ($alterDef['alter']['columns'] as $cname => $cdef) {
757 4
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
758
        }
759
760
        // Drop the columns and indexes.
761 7
        foreach ($alterDef['drop']['columns'] as $cname => $_) {
762
            $parts[] = 'drop '.$this->escape($cname);
763
        }
764 7
        foreach ($alterDef['drop']['indexes'] as $ixdef) {
765 2
            $parts[] = 'drop index '.$this->escape($ixdef['name']);
766
        }
767
768 7
        if (empty($parts)) {
769
            return false;
770
        }
771
772
        $sql = 'alter '.
773 7
            (self::val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
774 7
            'table '.$this->prefixTable($table)."\n  ".
775 7
            implode(",\n  ", $parts);
776
777 7
        $this->queryDefine($sql, $options);
778 7
    }
779
780
    /**
781
     * Get an array of column orders so that added columns can be slotted into their correct spot.
782
     *
783
     * @param array $cdefs An array of column definitions.
784
     * @return array Returns an array of column orders suitable for an `alter table` statement.
785
     */
786 7
    private function getColumnOrders($cdefs) {
787 7
        $orders = array_flip(array_keys($cdefs));
788
789 7
        $prev = ' first';
790 7
        foreach ($orders as $cname => &$value) {
791 7
            $value = $prev;
792 7
            $prev = ' after '.$this->escape($cname);
793
        }
794 7
        return $orders;
795
    }
796
797
    /**
798
     * Force a value into the appropriate php type based on its SQL type.
799
     *
800
     * @param mixed $value The value to force.
801
     * @param string $type The sqlite type name.
802
     * @return mixed Returns $value cast to the appropriate type.
803
     */
804 4
    protected function forceType($value, $type) {
805 4
        $type = strtolower($type);
806
807 4
        if ($type === 'null') {
808
            return null;
809 4
        } elseif ($type === 'boolean') {
810 1
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
811 3
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
812
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
813 3
            return filter_var($value, FILTER_VALIDATE_INT);
814
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
815
            'numeric', 'number', 'decimal(10,5)'])) {
816
            return filter_var($value, FILTER_VALIDATE_FLOAT);
817
        } else {
818
            return (string)$value;
819
        }
820
    }
821
822 45
    public function quote($value, string $column = ''): string {
823 45
        if (is_bool($value)) {
824 1
            return (string)(int)$value;
825 45
        } elseif ($value instanceof \DateTimeInterface) {
826 2
            $value = $value->format(self::MYSQL_DATE_FORMAT);
827 45
        } elseif ($value === null) {
828 2
            return 'null';
829
        }
830
831 44
        return parent::quote($value, $column);
832
    }
833
834
    /**
835
     * Convert a value into something usable as a PDO parameter.
836
     *
837
     * @param mixed $value The value to convert.
838
     * @return mixed Returns the converted value or the value itself if it's fine.
839
     */
840 44
    private function argValue($value) {
841 44
        if (is_bool($value)) {
842
            return (int)$value;
843 44
        } elseif ($value instanceof \DateTimeInterface) {
844
            return $value->format(self::MYSQL_DATE_FORMAT);
845
        } else {
846 44
            return $value;
847
        }
848
    }
849
}
850