MySqlDb   D
last analyzed

Complexity

Total Complexity 144

Size/Duplication

Total Lines 954
Duplicated Lines 8.39 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 84.98%

Importance

Changes 0
Metric Value
dl 80
loc 954
ccs 430
cts 506
cp 0.8498
rs 4
c 0
b 0
f 0
wmc 144
lcom 1
cbo 2

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A dropTable() 0 9 2
A backtick() 0 3 1
C query() 0 29 8
B getTableDef() 0 25 6
B getColumns() 0 57 9
A get() 0 5 1
C buildSelect() 0 61 12
C buildWhere() 18 75 22
A buildLike() 0 3 1
A bracketList() 8 14 3
A pdo() 0 18 2
A quoteVal() 0 10 3
A getDbName() 0 3 1
D columnTypeString() 0 43 9
B getIndexes() 0 47 6
B getAllTables() 0 30 5
A getTablenames() 21 21 1
A insert() 0 10 2
B buildInsert() 0 19 5
A buildUpsert() 0 14 2
B load() 0 26 4
A paramName() 0 4 1
A update() 0 10 2
A buildUpdate() 18 18 4
A delete() 0 15 4
B createTable() 0 25 5
A columnDefString() 0 17 4
A indexDefString() 0 12 4
C alterTable() 0 39 8
A getColumnOrders() 0 10 2
A forceType() 15 15 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MySqlDb often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MySqlDb, and based on these observations, apply Extract Interface, too.

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;
9
10
use PDO;
11
12
/**
13
 * A {@link Db} class for connecting to MySQL.
14
 */
15
class MySqlDb extends Db {
16
    /// Properties ///
17
18
    /**
19
     * @var \PDO
20
     */
21
    protected $pdo;
22
23
    protected $config;
24
25
    /// Methods ///
26
27
    /**
28
     * Initialize an instance of the {@link MySqlDb} class.
29
     *
30
     * @param array $config The database config.
31
     */
32
    public function __construct(array $config = []) {
33
        $this->config = $config;
34
    }
35
36
    /**
37
     * {@inheritdoc}
38
     */
39 6
    public function dropTable($tablename, array $options = []) {
40
        $sql = 'drop table '.
41 6
            (val(Db::OPTION_IGNORE, $options) ? 'if exists ' : '').
42 6
            $this->backtick($this->px.$tablename);
43 6
        $result = $this->query($sql, Db::QUERY_DEFINE);
44 6
        unset($this->tables[strtolower($tablename)]);
45
46 6
        return $result;
47
    }
48
49
    /**
50
     * Surround a field with backticks.
51
     *
52
     * @param string $field The field to backtick.
53
     * @return string Returns the field properly escaped and backticked.
54
     * @link http://www.php.net/manual/en/pdo.quote.php#112169
55
     */
56 62
    protected function backtick($field) {
57 62
        return '`'.str_replace('`', '``', $field).'`';
58
    }
59
60
    /**
61
     * Execute a query on the database.
62
     *
63
     * @param string $sql The sql query to execute.
64
     * @param string $type One of the Db::QUERY_* constants.
65
     *
66
     * Db::QUERY_READ
67
     * : The query reads from the database.
68
     *
69
     * Db::QUERY_WRITE
70
     * : The query writes to the database.
71
     *
72
     * Db::QUERY_DEFINE
73
     * : The query alters the structure of the datbase.
74
     *
75
     * @param array $options Additional options for the query.
76
     *
77
     * Db::OPTION_MODE
78
     * : Override {@link Db::$mode}.
79
     *
80
     * @return array|string|PDOStatement|int Returns the result of the query.
81
     *
82
     * array
83
     * : Returns an array when reading from the database and the mode is {@link Db::MODE_EXEC}.
84
     * string
85
     * : Returns the sql query when the mode is {@link Db::MODE_SQL}.
86
     * PDOStatement
87
     * : Returns a {@link \PDOStatement} when the mode is {@link Db::MODE_PDO}.
88
     * int
89
     * : Returns the number of rows affected when performing an update or an insert.
90
     */
91 58
    public function query($sql, $type = Db::QUERY_READ, $options = []) {
92 58
        $mode = val(Db::OPTION_MODE, $options, $this->mode);
93
94 58
        if ($mode & Db::MODE_ECHO) {
95
            echo trim($sql, "\n;").";\n\n";
96
        }
97 58
        if ($mode & Db::MODE_SQL) {
98
            return $sql;
99
        }
100
101 58
        $result = null;
102 58
        if ($mode & Db::MODE_EXEC) {
103 57
            $result = $this->pdo()->query($sql);
104
105 57
            if ($type == Db::QUERY_READ) {
106 47
                $result->setFetchMode(PDO::FETCH_ASSOC);
107 47
                $result = $result->fetchAll();
108 47
                $this->rowCount = count($result);
109 57
            } elseif (is_object($result) && method_exists($result, 'rowCount')) {
110 56
                $this->rowCount = $result->rowCount();
111 56
                $result = $this->rowCount;
112 56
            }
113 58
        } elseif ($mode & Db::MODE_PDO) {
114
            /* @var \PDOStatement $result */
115 5
            $result = $this->pdo()->prepare($sql);
116 5
        }
117
118 58
        return $result;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 44
    public function getTableDef($tablename) {
125 44
        $table = parent::getTableDef($tablename);
126 44
        if ($table || $table === null) {
127 44
            return $table;
128
        }
129
130 8
        $ltablename = strtolower($tablename);
131 8
        $table = val($ltablename, $this->tables, []);
132 8
        if (!isset($table['columns'])) {
133 8
            $columns = $this->getColumns($tablename);
134 8
            if ($columns === null) {
135
                // A table with no columns does not exist.
136 6
                $this->tables[$ltablename] = ['name' => $tablename];
137 6
                return null;
138
            }
139
140 8
            $table['columns'] = $columns;
141 8
        }
142 8
        if (!isset($table['indexes'])) {
143 8
            $table['indexes'] = $this->getIndexes($tablename);
144 8
        }
145 8
        $table['name'] = $tablename;
146 8
        $this->tables[$ltablename] = $table;
147 8
        return $table;
148
    }
149
150
    /**
151
     * Get the columns for tables and put them in {MySqlDb::$tables}.
152
     *
153
     * @param string $tablename The table to get the columns for or blank for all columns.
154
     * @return array|null Returns an array of columns if {@link $tablename} is specified, or null otherwise.
155
     */
156 5
    protected function getColumns($tablename = '') {
157 5
        $ltablename = strtolower($tablename);
158
        /* @var \PDOStatement $stmt */
159 5
        $stmt = $this->get(
160 5
            'information_schema.COLUMNS',
161
            [
162 5
                'TABLE_SCHEMA' => $this->getDbName(),
163 5
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
164 5
            ],
165
            [
166
                'columns' => [
167 5
                    'TABLE_NAME',
168 5
                    'COLUMN_TYPE',
169 5
                    'IS_NULLABLE',
170 5
                    'EXTRA',
171 5
                    'COLUMN_KEY',
172 5
                    'COLUMN_DEFAULT',
173
                    'COLUMN_NAME'
174 5
                ],
175 5
                Db::OPTION_MODE => Db::MODE_PDO,
176 5
                'escapeTable' => false,
177 5
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
178 5
            ]
179 5
        );
180
181 5
        $stmt->execute();
182 5
        $tablecolumns = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
183
184 5
        foreach ($tablecolumns as $ctablename => $cdefs) {
185 5
            $ctablename = strtolower(ltrim_substr($ctablename, $this->px));
186 5
            $columns = [];
187
188 5
            foreach ($cdefs as $cdef) {
189
                $column = [
190 5
                    'type' => $this->columnTypeString($cdef['COLUMN_TYPE']),
191 5
                    'required' => !force_bool($cdef['IS_NULLABLE']),
192 5
                ];
193 5
                if ($cdef['EXTRA'] === 'auto_increment') {
194 1
                    $column['autoincrement'] = true;
195 1
                }
196 5
                if ($cdef['COLUMN_KEY'] === 'PRI') {
197 3
                    $column['primary'] = true;
198 3
                }
199
200 5
                if ($cdef['COLUMN_DEFAULT'] !== null) {
201 3
                    $column['default'] = $this->forceType($cdef['COLUMN_DEFAULT'], $column['type']);
202 3
                }
203
204 5
                $columns[$cdef['COLUMN_NAME']] = $column;
205 5
            }
206 5
            $this->tables[$ctablename]['columns'] = $columns;
207 5
        }
208 5
        if ($ltablename && isset($this->tables[$ltablename]['columns'])) {
209 4
            return $this->tables[$ltablename]['columns'];
210
        }
211 4
        return null;
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 48
    public function get($tablename, array $where, array $options = []) {
218 48
        $sql = $this->buildSelect($tablename, $where, $options);
219 48
        $result = $this->query($sql, Db::QUERY_READ, $options);
220 48
        return $result;
221
    }
222
223
    /**
224
     * Build a sql select statement.
225
     *
226
     * @param string $table The name of the main table.
227
     * @param array $where The where filter.
228
     * @param array $options An array of additional query options.
229
     * @return string Returns the select statement as a string.
230
     * @see Db::get()
231
     */
232 52
    public function buildSelect($table, array $where, array $options = []) {
233 52
        $sql = '';
234
235
        // Build the select clause.
236 52
        if (isset($options['columns'])) {
237 10
            $columns = array();
238 10
            foreach ($options['columns'] as $value) {
239 10
                $columns[] = $this->backtick($value);
240 10
            }
241 10
            $sql .= 'select '.implode(', ', $columns);
242 10
        } else {
243 42
            $sql .= "select *";
244
        }
245
246
        // Build the from clause.
247 52
        if (val('escapeTable', $options, true)) {
248 46
            $sql .= "\nfrom ".$this->backtick($this->px.$table);
249 46
        } else {
250 6
            $sql .= "\nfrom $table";
251
        }
252
253
        // Build the where clause.
254 52
        $whereString = $this->buildWhere($where, Db::OP_AND);
255 52
        if ($whereString) {
256 46
            $sql .= "\nwhere ".$whereString;
257 46
        }
258
259
        // Build the order.
260 52
        if (isset($options['order'])) {
261 35
            $order = array_quick($options['order'], Db::ORDER_ASC);
262 35
            $orders = array();
263 35
            foreach ($order as $key => $value) {
264
                switch ($value) {
265 35
                    case Db::ORDER_ASC:
266 35
                    case Db::ORDER_DESC:
267 35
                        $orders[] = $this->backtick($key)." $value";
268 35
                        break;
269
                    default:
270
                        trigger_error("Invalid sort direction '$value' for column '$key'.", E_USER_WARNING);
271
                }
272 35
            }
273
274 35
            $sql .= "\norder by ".implode(', ', $orders);
275 35
        }
276
277
        // Build the limit, offset.
278 52
        $limit = 10;
279 52
        if (isset($options['limit'])) {
280 12
            $limit = (int)$options['limit'];
281 12
            $sql .= "\nlimit $limit";
282 12
        }
283
284 52
        if (isset($options['offset'])) {
285
            $sql .= ' offset '.((int)$options['offset']);
286 52
        } elseif (isset($options['page'])) {
287
            $offset = $limit * ($options['page'] - 1);
288
            $sql .= ' offset '.$offset;
289
        }
290
291 52
        return $sql;
292
    }
293
294
    /**
295
     * Build a where clause from a where array.
296
     *
297
     * @param array $where There where string.
298
     * This is an array in the form `['column' => 'value']` with more advanced options for non-equality comparisons.
299
     * @param string $op The logical operator to join multiple field comparisons.
300
     * @param bool $quotevals Whether or not to quote the where values.
301
     * @return string The where string.
302
     */
303 54
    protected function buildWhere($where, $op = Db::OP_AND, $quotevals = true) {
304 54
        static $map = array(Db::OP_GT => '>', Db::OP_GTE => '>=', Db::OP_LT => '<', Db::OP_LTE => '<=', Db::OP_LIKE => 'like');
305
306 54
        $result = '';
307 54
        foreach ($where as $column => $value) {
308 48
            $btcolumn = $this->backtick($column);
309
310 48
            if (is_array($value)) {
311 28
                if (isset($value[0])) {
312
                    // This is a short in syntax.
313 2
                    $value = [Db::OP_IN => $value];
314 2
                }
315
316 28
                foreach ($value as $vop => $rval) {
317 28
                    if ($result) {
318 6
                        $result .= "\n  $op ";
319 6
                    }
320
321
                    switch ($vop) {
322 28
                        case Db::OP_AND:
323 28
                        case Db::OP_OR:
324 4
                            $innerWhere = [$column => $rval];
325
                            $result .= "(\n  ".
326 4
                                $this->buildWhere($innerWhere, $vop, $quotevals).
327 4
                                "\n  )";
328 4
                            break;
329 28 View Code Duplication
                        case Db::OP_EQ:
330 6
                            if ($rval === null) {
331
                                $result .= "$btcolumn is null";
332 6
                            } elseif (is_array($rval)) {
333 2
                                $result .= "$btcolumn in ".$this->bracketList($rval);
334 2
                            } else {
335 4
                                $result .= "$btcolumn = ".$this->quoteVal($rval, $quotevals);
336
                            }
337 6
                            break;
338 24
                        case Db::OP_GT:
339 24
                        case Db::OP_GTE:
340 24
                        case Db::OP_LT:
341 24
                        case Db::OP_LTE:
342 12
                            $result .= "$btcolumn {$map[$vop]} ".$this->quoteVal($rval, $quotevals);
343 12
                            break;
344 12
                        case Db::OP_LIKE:
345 2
                            $result .= $this->buildLike($btcolumn, $rval, $quotevals);
346 2
                            break;
347 10
                        case Db::OP_IN:
348
                            // Quote the in values.
349 4
                            $rval = array_map(array($this->pdo, 'quote'), (array)$rval);
350 4
                            $result .= "$btcolumn in (".implode(', ', $rval).')';
351 4
                            break;
352 6 View Code Duplication
                        case Db::OP_NE:
353 6
                            if ($rval === null) {
354 2
                                $result .= "$btcolumn is not null";
355 6
                            } elseif (is_array($rval)) {
356 2
                                $result .= "$btcolumn not in ".$this->bracketList($rval);
357 2
                            } else {
358 2
                                $result .= "$btcolumn <> ".$this->quoteVal($rval, $quotevals);
359
                            }
360 6
                            break;
361
                    }
362 28
                }
363 28
            } else {
364 22
                if ($result) {
365 6
                    $result .= "\n  $op ";
366 6
                }
367
368
                // This is just an equality operator.
369 22
                if ($value === null) {
370 2
                    $result .= "$btcolumn is null";
371 2
                } else {
372 20
                    $result .= "$btcolumn = ".$this->quoteVal($value, $quotevals);
373
                }
374
            }
375 54
        }
376 54
        return $result;
377
    }
378
379
    /**
380
     * Build a like expression.
381
     *
382
     * @param string $column The column name.
383
     * @param mixed $value The right-hand value.
384
     * @param bool $quotevals Whether or not to quote the values.
385
     * @return string Returns the like expression.
386
     */
387 1
    protected function buildLike($column, $value, $quotevals) {
388 1
        return "$column like ".$this->quoteVal($value, $quotevals);
389
    }
390
391
    /**
392
     * Convert an array into a bracketed list suitable for MySQL clauses.
393
     *
394
     * @param array $row The row to expand.
395
     * @param string $quote The quotes to surroud the items with. There are two special cases.
396
     * ' (single quote)
397
     * : The row will be passed through {@link PDO::quote()}.
398
     * ` (backticks)
399
     * : The row will be passed through {@link MySqlDb::backtick()}.
400
     * @return string Returns the bracket list.
401
     */
402 60
    public function bracketList($row, $quote = "'") {
403
        switch ($quote) {
404 60 View Code Duplication
            case "'":
405 18
                $row = array_map([$this->pdo(), 'quote'], $row);
406 18
                $quote = '';
407 18
                break;
408 60 View Code Duplication
            case '`':
409 60
                $row = array_map([$this, 'backtick'], $row);
410 60
                $quote = '';
411 60
                break;
412
        }
413
414 60
        return "($quote".implode("$quote, $quote", $row)."$quote)";
415
    }
416
417
    /**
418
     * Gets the {@link PDO} object for this connection.
419
     *
420
     * @return \PDO
421
     */
422 31
    public function pdo() {
423 31
        $dsnParts = array_translate($this->config, ['host', 'dbname', 'port']);
424 31
        $dsn = 'mysql:'.implode_assoc(';', '=', $dsnParts);
425
426 31
        if (!isset($this->pdo)) {
427
            $this->pdo = new PDO(
428
                $dsn,
429
                val('username', $this->config, ''),
430
                val('password', $this->config, ''),
431
                [
432
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
433
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
434
                    PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8'
435
                ]
436
            );
437
        }
438 31
        return $this->pdo;
439
    }
440
441
    /**
442
     * Optionally quote a where value.
443
     *
444
     * @param mixed $value The value to quote.
445
     * @param bool $quote Whether or not to quote the value.
446
     * @return string Returns the value, optionally quoted.
447
     */
448 21
    public function quoteVal($value, $quote = true) {
449 21
        if ($value instanceof Literal) {
450
            /* @var Literal $value */
451
            return $value->getValue('mysql');
452 21
        } elseif ($quote) {
453 21
            return $this->pdo()->quote($value);
454
        } else {
455
            return $value;
456
        }
457
    }
458
459
    /**
460
     * Get the current database name.
461
     *
462
     * @return mixed
463
     */
464 5
    public function getDbName() {
465 5
        return val('dbname', $this->config);
466
    }
467
468
    /**
469
     * Parse a column type string and return it in a way that is suitible for a create/alter table statement.
470
     *
471
     * @param string $typeString The string to parse.
472
     * @return string Returns a canonical typestring.
473
     */
474 18
    protected function columnTypeString($typeString) {
475 18
        $type = null;
476
477 18
        if (substr($type, 0, 4) === 'enum') {
478
            // This is an enum which will come in as an array.
479
            if (preg_match_all("`'([^']+)'`", $typeString, $matches)) {
480
                $type = $matches[1];
481
            }
482
        } else {
483 18
            if (preg_match('`([a-z]+)\s*(?:\((\d+(?:\s*,\s*\d+)*)\))?\s*(unsigned)?`', $typeString, $matches)) {
484
                //         var_dump($matches);
485 18
                $str = $matches[1];
486 18
                $length = val(2, $matches);
487 18
                $unsigned = val(3, $matches);
488
489 18
                if (substr($str, 0, 1) == 'u') {
490
                    $unsigned = true;
491
                    $str = substr($str, 1);
492
                }
493
494
                // Remove the length from types without real lengths.
495 18
                if (in_array($str, array('tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'float', 'double'))) {
496 17
                    $length = null;
497 17
                }
498
499 18
                $type = $str;
500 18
                if ($length) {
501 8
                    $length = str_replace(' ', '', $length);
502 8
                    $type .= "($length)";
503 8
                }
504 18
                if ($unsigned) {
505
                    $type .= ' unsigned';
506
                }
507 18
            }
508
        }
509
510 18
        if (!$type) {
511
            debug_print_backtrace();
512
            trigger_error("Couldn't parse type $typeString", E_USER_ERROR);
513
        }
514
515 18
        return $type;
516
    }
517
518
    /**
519
     * Get the indexes from the database.
520
     *
521
     * @param string $tablename The name of the table to get the indexes for or an empty string to get all indexes.
522
     * @return array|null
523
     */
524 5
    protected function getIndexes($tablename = '') {
525 5
        $ltablename = strtolower($tablename);
526
        /* @var \PDOStatement */
527 5
        $stmt = $this->get(
528 5
            'information_schema.STATISTICS',
529
            [
530 5
                'TABLE_SCHEMA' => $this->getDbName(),
531 5
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
532 5
            ],
533
            [
534
                'columns' => [
535 5
                    'INDEX_NAME',
536 5
                    'TABLE_NAME',
537 5
                    'NON_UNIQUE',
538
                    'COLUMN_NAME'
539 5
                ],
540 5
                'escapeTable' => false,
541 5
                'order' => ['TABLE_NAME', 'INDEX_NAME', 'SEQ_IN_INDEX'],
542 5
                Db::OPTION_MODE => Db::MODE_PDO
543 5
            ]
544 5
        );
545
546 5
        $stmt->execute();
547 5
        $indexDefs = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
548
549 5
        foreach ($indexDefs as $indexName => $indexRows) {
550 5
            $row = reset($indexRows);
551 5
            $itablename = strtolower(ltrim_substr($row['TABLE_NAME'], $this->px));
552
            $index = [
553 5
                'name' => $indexName,
554 5
                'columns' => array_column($indexRows, 'COLUMN_NAME')
555 5
            ];
556
557 5
            if ($indexName === 'PRIMARY') {
558 3
                $index['type'] = Db::INDEX_PK;
559 3
                $this->tables[$itablename]['indexes'][Db::INDEX_PK] = $index;
560 3
            } else {
561 3
                $index['type'] = $row['NON_UNIQUE'] ? Db::INDEX_IX : Db::INDEX_UNIQUE;
562 3
                $this->tables[$itablename]['indexes'][] = $index;
563
            }
564 5
        }
565
566 5
        if ($ltablename) {
567 4
            return valr([$ltablename, 'indexes'], $this->tables, []);
568
        }
569 1
        return null;
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 2
    public function getAllTables($withDefs = false) {
576 2
        $tables = parent::getAllTables($withDefs);
577 2
        if ($tables !== null) {
578 2
            return $tables;
579
        }
580
581
        // Grab the tablenames first.
582 2
        if ($this->allTablesFetched & Db::FETCH_TABLENAMES) {
583 2
            $tablenames = array_keys($this->tables);
584 2
        } else {
585
            $tablenames = $this->getTablenames();
586
            $this->tables = [];
587
            foreach ($tablenames as $tablename) {
588
                $this->tables[strtolower($tablename)] = ['name' => $tablename];
589
            }
590
            $this->allTablesFetched = Db::FETCH_TABLENAMES;
591
        }
592
593 2
        if (!$withDefs) {
594
            return $tablenames;
595
        }
596
597 2
        $this->getColumns();
598 2
        $this->allTablesFetched |= Db::FETCH_COLUMNS;
599
600 2
        $this->getIndexes();
601 2
        $this->allTablesFetched |= Db::FETCH_INDEXES;
602
603 2
        return $this->tables;
604
    }
605
606
    /**
607
     * Get the all of tablenames in the database.
608
     *
609
     * @return array Returns an array of table names with prefixes stripped.
610
     */
611 View Code Duplication
    protected function getTablenames() {
612
        // Get the table names.
613
        $tables = (array)$this->get(
614
            'information_schema.TABLES',
615
            [
616
                'TABLE_SCHEMA' => $this->getDbName(),
617
                'TABLE_NAME' => [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
618
            ],
619
            [
620
                'columns' => ['TABLE_NAME'],
621
                'escapeTable' => false
622
            ]
623
        );
624
625
        // Strip the table prefixes.
626
        $tables = array_map(function ($name) {
627
            return ltrim_substr($name, $this->px);
628
        }, array_column($tables, 'TABLE_NAME'));
629
630
        return $tables;
631
    }
632
633
    /**
634
     * {@inheritdoc}
635
     */
636 14
    public function insert($tablename, array $rows, array $options = []) {
637 14
        $sql = $this->buildInsert($tablename, $rows, true, $options);
638 14
        $this->query($sql, Db::QUERY_WRITE);
639 14
        $id = $this->pdo()->lastInsertId();
640 14
        if (is_numeric($id)) {
641 14
            return (int)$id;
642
        } else {
643
            return $id;
644
        }
645
    }
646
647
    /**
648
     * Build an insert statement.
649
     *
650
     * @param string $tablename The name of the table to insert to.
651
     * @param array $row The row to insert.
652
     * @param bool $quotevals Whether or not to quote the values.
653
     * @param array $options An array of options for the insert. See {@link Db::insert} for the options.
654
     * @return string Returns the the sql string of the insert statement.
655
     */
656 24
    protected function buildInsert($tablename, array $row, $quotevals = true, $options = []) {
657 24
        if (val(Db::OPTION_UPSERT, $options)) {
658 2
            return $this->buildUpsert($tablename, $row, $quotevals, $options);
659 24
        } elseif (val(Db::OPTION_IGNORE, $options)) {
660 2
            $sql = 'insert ignore ';
661 24
        } elseif (val(Db::OPTION_REPLACE, $options)) {
662 2
            $sql = 'replace ';
663 2
        } else {
664 22
            $sql = 'insert ';
665
        }
666 24
        $sql .= $this->backtick($this->px.$tablename);
667
668
        // Add the list of values.
669
        $sql .=
670 24
            "\n".$this->bracketList(array_keys($row), '`').
671 24
            "\nvalues".$this->bracketList($row, $quotevals ? "'" : '');
672
673 24
        return $sql;
674
    }
675
676
    /**
677
     * Build an upsert statement.
678
     *
679
     * An upsert statement is an insert on duplicate key statement in MySQL.
680
     *
681
     * @param string $tablename The name of the table to update.
682
     * @param array $row The row to insert or update.
683
     * @param bool $quotevals Whether or not to quote the values in the row.
684
     * @param array $options An array of additional query options.
685
     * @return string Returns the upsert statement as a string.
686
     */
687 2
    protected function buildUpsert($tablename, array $row, $quotevals = true, $options = []) {
688
        // Build the initial insert statement first.
689 2
        unset($options[Db::OPTION_UPSERT]);
690 2
        $sql = $this->buildInsert($tablename, $row, $quotevals, $options);
691
692
        // Add the duplicate key stuff.
693 2
        $updates = [];
694 2
        foreach ($row as $key => $value) {
695 2
            $updates[] = $this->backtick($key).' = values('.$this->backtick($key).')';
696 2
        }
697 2
        $sql .= "\non duplicate key update ".implode(', ', $updates);
698
699 2
        return $sql;
700
    }
701
702
    /**
703
     * {@inheritdoc}
704
     */
705 34
    public function load($tablename, $rows, array $options = []) {
706 34
        $count = 0;
707 34
        $first = true;
708 34
        $spec = [];
709 34
        $stmt = null;
710
711
        // Loop over the rows and insert them with the statement.
712 34
        foreach ($rows as $row) {
713 34
            if ($first) {
714
                // Build the insert statement from the first row.
715 34
                foreach ($row as $key => $value) {
716 34
                    $spec[$key] = $this->paramName($key);
717 34
                }
718
719 34
                $sql = $this->buildInsert($tablename, $spec, false, $options);
720 34
                $stmt = $this->pdo()->prepare($sql);
721 34
                $first = false;
722 34
            }
723
724 34
            $params = array_translate($row, $spec);
725 34
            $stmt->execute($params);
726 34
            $count += $stmt->rowCount();
727 34
        }
728
729 34
        return $count;
730
    }
731
732
    /**
733
     * Make a valid pdo parameter name from a string.
734
     *
735
     * This method replaces invalid placeholder characters with underscores.
736
     *
737
     * @param string $name The name to replace.
738
     * @return string
739
     */
740 34
    protected function paramName($name) {
741 34
        $result = ':'.preg_replace('`[^a-zA-Z0-9_]`', '_', $name);
742 34
        return $result;
743
    }
744
745
    /**
746
     * {@inheritdoc}
747
     */
748 6
    public function update($tablename, array $set, array $where, array $options = []) {
749 6
        $sql = $this->buildUpdate($tablename, $set, $where, true, $options);
750 6
        $result = $this->query($sql, Db::QUERY_WRITE);
751
752 6
        if ($result instanceof \PDOStatement) {
753
            /* @var \PDOStatement $result */
754
            return $result->rowCount();
755
        }
756 6
        return $result;
757
    }
758
759
    /**
760
     * Build a sql update statement.
761
     *
762
     * @param string $tablename The name of the table to update.
763
     * @param array $set An array of columns to set.
764
     * @param array $where The where filter.
765
     * @param bool $quotevals Whether or not to quote the values.
766
     * @param array $options Additional options for the query.
767
     * @return string Returns the update statement as a string.
768
     */
769 2 View Code Duplication
    protected function buildUpdate($tablename, array $set, array $where, $quotevals = true, array $options = []) {
770
        $sql = 'update '.
771 2
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
772 2
            $this->backtick($this->px.$tablename).
773 2
            "\nset\n  ";
774
775 2
        $parts = [];
776 2
        foreach ($set as $key => $value) {
777 2
            $parts[] = $this->backtick($key).' = '.$this->quoteVal($value, $quotevals);
778 2
        }
779 2
        $sql .= implode(",\n  ", $parts);
780
781 2
        if (!empty($where)) {
782 2
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND, $quotevals);
783 2
        }
784
785 2
        return $sql;
786
    }
787
788
    /**
789
     * {@inheritdoc}
790
     */
791 30
    public function delete($tablename, array $where, array $options = []) {
792 30
        if (val(Db::OPTION_TRUNCATE, $options)) {
793
            if (!empty($where)) {
794
                throw new \InvalidArgumentException("You cannot truncate $tablename with a where filter.", 500);
795
            }
796
            $sql = 'truncate table '.$this->backtick($this->px.$tablename);
797
        } else {
798 30
            $sql = 'delete from '.$this->backtick($this->px.$tablename);
799
800 30
            if (!empty($where)) {
801
                $sql .= "\nwhere ".$this->buildWhere($where);
802
            }
803
        }
804 30
        return $this->query($sql, Db::QUERY_WRITE);
805
    }
806
807
    /**
808
     * {@inheritdoc}
809
     */
810 8
    protected function createTable($tablename, array $tabledef, array $options = []) {
811
        // The table doesn't exist so this is a create table.
812 8
        $parts = array();
813 8
        foreach ($tabledef['columns'] as $name => $def) {
814 8
            $parts[] = $this->columnDefString($name, $def);
815 8
        }
816
817 8
        foreach (val('indexes', $tabledef, []) as $index) {
818 8
            $indexDef = $this->indexDefString($tablename, $index);
819 8
            if ($indexDef) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexDef of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
820 8
                $parts[] = $indexDef;
821 8
            }
822 8
        }
823
824 8
        $fullTablename = $this->backtick($this->px.$tablename);
825 8
        $sql = "create table $fullTablename (\n  ".
826 8
            implode(",\n  ", $parts).
827 8
            "\n)";
828
829 8
        if (val('collate', $options)) {
830
            $sql .= "\n collate {$options['collate']}";
831
        }
832
833 8
        $this->query($sql, Db::QUERY_DEFINE);
834 8
    }
835
836
    /**
837
     * Construct a column definition string from an array defintion.
838
     *
839
     * @param string $name The name of the column.
840
     * @param array $def The column definition.
841
     * @return string Returns a string representing the column definition.
842
     */
843 8
    protected function columnDefString($name, array $def) {
844 8
        $result = $this->backtick($name).' '.$this->columnTypeString($def['type']);
845
846 8
        if (val('required', $def)) {
847 7
            $result .= ' not null';
848 7
        }
849
850 8
        if (isset($def['default'])) {
851 5
            $result .= ' default '.$this->quoteVal($def['default']);
852 5
        }
853
854 8
        if (val('autoincrement', $def)) {
855 2
            $result .= ' auto_increment';
856 2
        }
857
858 8
        return $result;
859
    }
860
861
    /**
862
     * Return the SDL string that defines an index.
863
     *
864
     * @param string $tablename The name of the table that the index is on.
865
     * @param array $def The index defintion. This definition should have the following keys.
866
     *
867
     * columns
868
     * : An array of columns in the index.
869
     * type
870
     * : One of "index", "unique", or "primary".
871
     * @return null|string Returns the index string or null if the index is not correct.
872
     */
873 8
    protected function indexDefString($tablename, array $def) {
874 8
        $indexName = $this->backtick($this->buildIndexName($tablename, $def));
875 8
        switch (val('type', $def, Db::INDEX_IX)) {
876 8
            case Db::INDEX_IX:
877 5
                return "index $indexName ".$this->bracketList($def['columns'], '`');
878 5
            case Db::INDEX_UNIQUE:
879 1
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
880 5
            case Db::INDEX_PK:
881 5
                return "primary key ".$this->bracketList($def['columns'], '`');
882
        }
883
        return null;
884
    }
885
886
    /**
887
     * {@inheritdoc}
888
     */
889 4
    protected function alterTable($tablename, array $alterdef, array $options = []) {
890 4
        $columnOrders = $this->getColumnOrders($alterdef['def']['columns']);
891 4
        $parts = [];
892
893
        // Add the columns and indexes.
894 4
        foreach ($alterdef['add']['columns'] as $cname => $cdef) {
895
            // Figure out the order of the column.
896 2
            $pos = val($cname, $columnOrders, '');
897 2
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
898 4
        }
899 4
        foreach ($alterdef['add']['indexes'] as $ixdef) {
900 3
            $parts[] = 'add '.$this->indexDefString($tablename, $ixdef);
901 4
        }
902
903
        // Alter the columns.
904 4
        foreach ($alterdef['alter']['columns'] as $cname => $cdef) {
905 4
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
906 4
        }
907
908
        // Drop the columns and indexes.
909 4
        foreach ($alterdef['drop']['columns'] as $cname => $_) {
910 1
            $parts[] = 'drop '.$this->backtick($cname);
911 4
        }
912 4
        foreach ($alterdef['drop']['indexes'] as $ixdef) {
913 3
            $parts[] = 'drop index '.$this->backtick($ixdef['name']);
914 4
        }
915
916 4
        if (empty($parts)) {
917
            return false;
918
        }
919
920
        $sql = 'alter '.
921 4
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
922 4
            'table '.$this->backtick($this->px.$tablename)."\n  ".
923 4
            implode(",\n  ", $parts);
924
925 4
        $result = $this->query($sql, Db::QUERY_DEFINE);
926 4
        return $result;
927
    }
928
929
    /**
930
     * Get an array of column orders so that added columns can be slotted into their correct spot.
931
     *
932
     * @param array $cdefs An array of column definitions.
933
     * @return array Returns an array of column orders suitable for an `alter table` statement.
934
     */
935 4
    protected function getColumnOrders($cdefs) {
936 4
        $orders = array_flip(array_keys($cdefs));
937
938 4
        $prev = ' first';
939 4
        foreach ($orders as $cname => &$value) {
940 4
            $value = $prev;
941 4
            $prev = ' after '.$this->backtick($cname);
942 4
        }
943 4
        return $orders;
944
    }
945
946
    /**
947
     * Force a value into the appropriate php type based on its Sqlite type.
948
     *
949
     * @param mixed $value The value to force.
950
     * @param string $type The sqlite type name.
951
     * @return mixed Returns $value cast to the appropriate type.
952
     */
953 3 View Code Duplication
    protected function forceType($value, $type) {
954 3
        $type = strtolower($type);
955
956 3
        if ($type === 'null') {
957
            return null;
958 3
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
959 3
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
960 3
            return force_int($value);
961
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
962
            'numeric', 'decimal(10,5)'])) {
963
            return floatval($value);
964
        } else {
965
            return (string)$value;
966
        }
967
    }
968
}
969