Completed
Push — master ( c2bd55...6b7717 )
by Todd
09:04
created

SqliteDb::dropIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4286
cc 1
eloc 4
nc 1
nop 1
crap 1
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 SQLite.
14
 */
15
class SqliteDb extends MySqlDb {
16
    /**
17
     * {@inheritdoc}
18
     */
19 4
    protected function alterTable($tablename, array $alterdef, array $options = []) {
20 4
        $this->alterTableMigrate($tablename, $alterdef, $options);
21 4
    }
22
23
    /**
24
     * Alter a table by creating a new table and copying the old table's data to it.
25
     *
26
     * @param string $tablename The table to alter.
27
     * @param array $alterDef The new definition.
28
     * @param array $options An array of options for the migration.
29
     */
30 4
    protected function alterTableMigrate($tablename, array $alterDef, array $options = []) {
31 4
        $currentDef = $this->getTableDef($tablename);
32
33
        // Merge the table definitions if we aren't dropping stuff.
34 4
        if (!val(Db::OPTION_DROP, $options)) {
35 3
            $tableDef = $this->mergeTableDefs($currentDef, $alterDef);
36 3
        } else {
37 1
            $tableDef = $alterDef['def'];
38
        }
39
40
        // Drop all of the indexes on the current table.
41 4
        foreach (val('indexes', $currentDef, []) as $indexDef) {
42 4
            if (val('type', $indexDef, Db::INDEX_IX) === Db::INDEX_IX) {
43 2
                $this->dropIndex($indexDef['name']);
44 2
            }
45 4
        }
46
47 4
        $tmpTablename = $tablename.'_'.time();
48
49
        // Rename the current table.
50 4
        $this->renameTable($tablename, $tmpTablename);
51
52
        // Create the new table.
53 4
        $this->createTable($tablename, $tableDef, $options);
54
55
        // Figure out the columns that we can insert.
56 4
        $columns = array_keys(array_intersect_key($tableDef['columns'], $currentDef['columns']));
57
58
        // Build the insert/select statement.
59 4
        $sql = 'insert into '.$this->backtick($this->px.$tablename)."\n".
60 4
            $this->bracketList($columns, '`')."\n".
61 4
            $this->buildSelect($tmpTablename, [], ['columns' => $columns]);
62
63 4
        $this->query($sql, Db::QUERY_WRITE);
64
65
        // Drop the temp table.
66 4
        $this->dropTable($tmpTablename);
67 4
    }
68
69
    /**
70
     * Rename a table.
71
     *
72
     * @param string $oldname The old name of the table.
73
     * @param string $newname The new name of the table.
74
     */
75 4
    protected function renameTable($oldname, $newname) {
76
        $renameSql = 'alter table '.
77 4
            $this->backtick($this->px.$oldname).
78 4
            ' rename to '.
79 4
            $this->backtick($this->px.$newname);
80 4
        $this->query($renameSql, Db::QUERY_WRITE);
81 4
    }
82
83
    /**
84
     * Merge a table def with its alter def so that no columns/indexes are lost in an alter.
85
     *
86
     * @param array $tableDef The table def.
87
     * @param array $alterDef The alter def.
88
     * @return array The new table def.
89
     */
90 3
    protected function mergeTableDefs(array $tableDef, array $alterDef) {
91 3
        $result = $tableDef;
92
93 3
        $result['columns'] = array_merge($result['columns'], $alterDef['def']['columns']);
94 3
        $result['indexes'] = array_merge($result['indexes'], $alterDef['add']['indexes']);
95
96 3
        return $result;
97
    }
98
99
    /**
100
     * Drop an index.
101
     *
102
     * @param string $indexName The name of the index to drop.
103
     */
104 2
    protected function dropIndex($indexName) {
105
        $sql = 'drop index if exists '.
106 2
            $this->backtick($indexName);
107 2
        $this->query($sql, Db::QUERY_DEFINE);
108 2
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 24
    protected function buildInsert($tablename, array $row, $quotevals = true, $options = []) {
114 24
        if (val(Db::OPTION_UPSERT, $options)) {
115
            throw new \Exception("Upsert is not supported.");
116 24
        } elseif (val(Db::OPTION_IGNORE, $options)) {
117 2
            $sql = 'insert or ignore into ';
118 24
        } elseif (val(Db::OPTION_REPLACE, $options)) {
119 2
            $sql = 'insert or replace into ';
120 2
        } else {
121 22
            $sql = 'insert into ';
122
        }
123 24
        $sql .= $this->backtick($this->px.$tablename);
124
125
        // Add the list of values.
126
        $sql .=
127 24
            "\n".$this->bracketList(array_keys($row), '`').
128 24
            "\nvalues".$this->bracketList($row, $quotevals ? "'" : '');
129
130 24
        return $sql;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 1
    protected function buildLike($column, $value, $quotevals) {
137 1
        return "$column like ".$this->quoteVal($value, $quotevals)." escape '\\'";
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 4 View Code Duplication
    protected function buildUpdate($tablename, array $set, array $where, $quotevals = true, array $options = []) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
144
        $sql = 'update '.
145 4
            (val(Db::OPTION_IGNORE, $options) ? 'or ignore ' : '').
146 4
            $this->backtick($this->px.$tablename).
147 4
            "\nset\n  ";
148
149 4
        $parts = [];
150 4
        foreach ($set as $key => $value) {
151 4
            $parts[] = $this->backtick($key).' = '.$this->quoteVal($value, $quotevals);
152 4
        }
153 4
        $sql .= implode(",\n  ", $parts);
154
155 4
        if (!empty($where)) {
156 4
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND, $quotevals);
157 4
        }
158
159 4
        return $sql;
160
    }
161
162
    /**
163
     * Construct a column definition string from an array defintion.
164
     *
165
     * @param string $name The name of the column.
166
     * @param array $def The column definition.
167
     * @return string Returns a string representing the column definition.
168
     */
169 8
    protected function columnDefString($name, array $def) {
170
        // Auto-increments MUST be of type integer.
171 8
        if (val('autoincrement', $def)) {
172 2
            $def['type'] = 'integer';
173 2
        }
174
175 8
        $result = $this->backtick($name).' '.$this->columnTypeString($def['type']);
176
177 8
        if (val('primary', $def) && val('autoincrement', $def)) {
178
//            if (val('autoincrement', $def)) {
179 2
                $result .= ' primary key autoincrement';
180 2
                $def['primary'] = true;
181
//            }
182 8
        } elseif (isset($def['default'])) {
183 5
            $result .= ' default '.$this->quoteVal($def['default']);
184 8
        } elseif (val('required', $def)) {
185 7
            $result .= ' not null';
186 7
        }
187
188 8
        return $result;
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 8
    protected function createTable($tablename, array $tabledef, array $options = []) {
195 8
        $parts = [];
196
197
        // Make sure the primary key columns are defined first and in order.
198 8
        $autoinc = false;
199 8
        if (isset($tabledef['indexes']['primary'])) {
200 5
            $pkIndex = $tabledef['indexes']['primary'];
201 5
            foreach ($pkIndex['columns'] as $column) {
202 5
                $cdef = $tabledef['columns'][$column];
203 5
                $parts[] = $this->columnDefString($column, $cdef);
204 5
                $autoinc |= val('autoincrement', $cdef, false);
205 5
                unset($tabledef['columns'][$column]);
206 5
            }
207 5
        }
208
209 8
        foreach ($tabledef['columns'] as $name => $cdef) {
210 7
            $parts[] = $this->columnDefString($name, $cdef);
211 8
        }
212
213
        // Add the prinary key index.
214 8
        if (isset($pkIndex) && !$autoinc) {
215 3
            $parts[] = 'primary key '.$this->bracketList($pkIndex['columns'], '`');
216 3
        }
217
218 8
        $fullTablename = $this->backtick($this->px.$tablename);
219 8
        $sql = "create table $fullTablename (\n  ".
220 8
            implode(",\n  ", $parts).
221 8
            "\n)";
222
223 8
        $this->query($sql, Db::QUERY_DEFINE);
224
225
        // Add the rest of the indexes.
226 8
        foreach (val('indexes', $tabledef, []) as $index) {
227 8
            if (val('type', $index, Db::INDEX_IX) !== Db::INDEX_PK) {
228 5
                $this->createIndex($tablename, $index, $options);
229 5
            }
230 8
        }
231 8
    }
232
233
    /**
234
     * Create an index.
235
     *
236
     * @param string $tablename The name of the table to create the index on.
237
     * @param array $indexDef The index definition.
238
     * @param array $options Additional options for the index creation.
239
     */
240 5
    public function createIndex($tablename, array $indexDef, $options = []) {
241
        $sql = 'create '.
242 5
            (val('type', $indexDef) === Db::INDEX_UNIQUE ? 'unique ' : '').
243 5
            'index '.
244 5
            (val(Db::OPTION_IGNORE, $options) ? 'if not exists ' : '').
245 5
            $this->buildIndexName($tablename, $indexDef).
246 5
            ' on '.
247 5
            $this->backtick($this->px.$tablename).
248 5
            $this->bracketList($indexDef['columns'], '`');
249
250 5
        $this->query($sql, Db::QUERY_DEFINE);
251 5
    }
252
253
    /**
254
     * Force a value into the appropriate php type based on its Sqlite type.
255
     *
256
     * @param mixed $value The value to force.
257
     * @param string $type The sqlite type name.
258
     * @return mixed Returns $value cast to the appropriate type.
259
     */
260 View Code Duplication
    protected function forceType($value, $type) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
261
        $type = strtolower($type);
262
263
        if ($type === 'null') {
264
            return null;
265
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
266
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
267
            return force_int($value);
268
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
269
            'numeric', 'decimal(10,5)'])) {
270
            return floatval($value);
271
        } else {
272
            return (string)$value;
273
        }
274
    }
275
276
    /**
277
     * Get the columns for tables and put them in {MySqlDb::$tables}.
278
     *
279
     * @param string $tablename The table to get the columns for or blank for all columns.
280
     * @return array|null Returns an array of columns if {@link $tablename} is specified, or null otherwise.
281
     */
282 5
    protected function getColumns($tablename = '') {
283 5
        if (!$tablename) {
284 1
            $tablenames = $this->getTablenames();
285 1
            foreach ($tablenames as $tablename) {
286 1
                $this->getColumns($tablename);
287 1
            }
288 1
        }
289
290 5
        $cdefs = (array)$this->query('pragma table_info('.$this->quoteVal($this->px.$tablename).')');
291 5
        if (empty($cdefs)) {
292 3
            return null;
293
        }
294
295 5
        $columns = [];
296 5
        $pk = [];
297 5
        foreach ($cdefs as $cdef) {
298
            $column = [
299 5
                'type' => $this->columnTypeString($cdef['type']),
300 5
                'required' => force_bool($cdef['notnull']),
301 5
            ];
302 5
            if ($cdef['pk']) {
303 3
                $pk[] = $cdef['name'];
304 3
                if (strcasecmp($cdef['type'], 'integer') === 0) {
305 1
                    $column['autoincrement'] = true;
306 1
                } else {
307 3
                    $column['primary'] = true;
308
                }
309 3
            }
310 5
            if ($cdef['dflt_value'] !== null) {
311 3
                $column['default'] = $cdef['dflt_value'];
312 3
            }
313 5
            $columns[$cdef['name']] = $column;
314 5
        }
315 5
        $tdef = ['columns' => $columns];
316 5
        if (!empty($pk)) {
317 3
            $tdef['indexes'][Db::INDEX_PK] = [
318 3
                'columns' => $pk,
319
                'type' => Db::INDEX_PK
320 3
            ];
321 3
        }
322 5
        $this->tables[$tablename] = $tdef;
323 5
        return $columns;
324
    }
325
326
    /**
327
     * Get the indexes from the database.
328
     *
329
     * @param string $tablename The name of the table to get the indexes for or an empty string to get all indexes.
330
     * @return array|null
331
     */
332 5
    protected function getIndexes($tablename = '') {
333 5
        if (!$tablename) {
334 1
            $tablenames = $this->getTablenames();
335 1
            foreach ($tablenames as $tablename) {
336 1
                $this->getIndexes($tablename);
337 1
            }
338 1
        }
339
340 5
        $pk = valr(['indexes', Db::INDEX_PK], $this->tables[$tablename]);
341
342
        // Reset the index list for the table.
343 5
        $this->tables[$tablename]['indexes'] = [];
344
345 5
        if ($pk) {
346 3
            $this->tables[$tablename]['indexes'][Db::INDEX_PK] = $pk;
347 3
        }
348
349 5
        $indexInfos = (array)$this->query('pragma index_list('.$this->quoteVal($this->px.$tablename).')');
350 5
        foreach ($indexInfos as $row) {
351 5
            $indexName = $row['name'];
352 5
            if ($row['unique']) {
353 3
                $type = Db::INDEX_UNIQUE;
354 3
            } else {
355 3
                $type = Db::INDEX_IX;
356
            }
357
358
            // Query the columns in the index.
359 5
            $columns = (array)$this->query('pragma index_info('.$this->quoteVal($indexName).')');
360
361
            $index = [
362 5
                'name' => $indexName,
363 5
                'columns' => array_column($columns, 'name'),
364
                'type' => $type
365 5
            ];
366 5
            $this->tables[$tablename]['indexes'][] = $index;
367 5
        }
368
369 5
        return $this->tables[$tablename]['indexes'];
370
    }
371
372
    /**
373
     * Get the primary or secondary keys from the given rows.
374
     *
375
     * @param string $tablename The name of the table.
376
     * @param array $row The row to examine.
377
     * @param bool $quick Whether or not to quickly look for <tablename>ID for the primary key.
378
     * @return array|null Returns the primary keys and values from {@link $rows} or null if the primary key isn't found.
379
     */
380 2
    protected function getPKValue($tablename, array $row, $quick = false) {
381 2
        if ($quick && isset($row[$tablename.'ID'])) {
382 1
            return [$tablename.'ID' => $row[$tablename.'ID']];
383
        }
384
385 1
        $tdef = $this->getTableDef($tablename);
386 1
        if (isset($tdef['indexes'][Db::INDEX_PK]['columns'])) {
387 1
            $pkColumns = array_flip($tdef['indexes'][Db::INDEX_PK]['columns']);
388 1
            $cols = array_intersect_key($row, $pkColumns);
389 1
            if (count($cols) === count($pkColumns)) {
390 1
                return $cols;
391
            }
392
        }
393
394
        return null;
395
    }
396
397
    /**
398
     * Get the all of tablenames in the database.
399
     *
400
     * @return array Returns an array of table names with prefixes stripped.
401
     */
402 1 View Code Duplication
    protected function getTablenames() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
403
        // Get the table names.
404 1
        $tables = (array)$this->get(
405 1
            'sqlite_master',
406
            [
407 1
                'type' => 'table',
408 1
                'name' => [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
409 1
            ],
410
            [
411 1
                'columns' => ['name'],
412
                'escapeTable' => false
413 1
            ]
414 1
        );
415
416
        // Strip the table prefixes.
417 1
        $tables = array_map(function ($name) {
418 1
            return ltrim_substr($name, $this->px);
419 1
        }, array_column($tables, 'name'));
420
421 1
        return $tables;
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427 7
    public function insert($tablename, array $rows, array $options = []) {
428
        // Sqlite doesn't support upsert so do upserts manually.
429 7
        if (val(Db::OPTION_UPSERT, $options)) {
430 2
            unset($options[Db::OPTION_UPSERT]);
431
432 2
            $keys = $this->getPKValue($tablename, $rows, true);
433 2
            if (!$keys) {
434
                throw new \Exception("Cannot upsert with no key.", 500);
435
            }
436
            // Try updating first.
437 2
            $updated = $this->update(
438 2
                $tablename,
439 2
                array_diff_key($rows, $keys),
440 2
                $keys,
441
                $options
442 2
            );
443 2
            if ($updated) {
444
                // Updated.
445 2
                if (count($keys) === 1) {
446 1
                    return array_pop($keys);
447
                } else {
448 1
                    return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type of the parent method Garden\Db\MySqlDb::insert of type integer|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
449
                }
450
            }
451 1
        }
452
453 7
        $result = parent::insert($tablename, $rows, $options);
454 7
        return $result;
455
    }
456
457
    /**
458
     * Gets the {@link PDO} object for this connection.
459
     *
460
     * @return \PDO
461
     */
462 31
    public function pdo() {
463 31
        $dsn = 'sqlite:'.$this->config['path'];
464
465 31
        if (!isset($this->pdo)) {
466
            $this->pdo = new PDO($dsn, val('username', $this->config, null), val('password', $this->config, null));
467
        }
468 31
        return $this->pdo;
469
    }
470
471
    /**
472
     * Optionally quote a where value.
473
     *
474
     * @param mixed $value The value to quote.
475
     * @param bool $quote Whether or not to quote the value.
476
     * @return string Returns the value, optionally quoted.
477
     */
478 21
    public function quoteVal($value, $quote = true) {
479 21
        if ($value instanceof Literal) {
480
            /* @var Literal $value */
481
            return $value->getValue('mysql');
482 21
        } elseif (in_array(gettype($value), ['integer', 'double'])) {
483 20
            return (string)$value;
484 9
        } elseif ($value === true) {
485
            return '1';
486 9
        } elseif ($value === false) {
487
            return '0';
488 9
        } elseif ($quote) {
489 9
            return $this->pdo()->quote($value);
490
        } else {
491
            return $value;
492
        }
493
    }
494
}
495