Test Failed
Pull Request — master (#12)
by Todd
02:13
created

SqliteDb::dropIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 SQLite.
17
 */
18
class SqliteDb extends MySqlDb {
19
    /**
20
     * {@inheritdoc}
21
     */
22 7
    protected function alterTableDb(array $alterDef, array $options = []) {
23 7
        $this->alterTableMigrate($alterDef, $options);
24 7
    }
25
26
    /**
27
     * Alter a table by creating a new table and copying the old table's data to it.
28
     *
29
     * @param array $alterDef The new definition.
30
     * @param array $options An array of options for the migration.
31
     */
32 7
    private function alterTableMigrate(array $alterDef, array $options = []) {
33 7
        $table = $alterDef['name'];
34 7
        $currentDef = $this->fetchTableDef($table);
35
36
        // Merge the table definitions if we aren't dropping stuff.
37 7
        if (!self::val(Db::OPTION_DROP, $options)) {
38 6
            $tableDef = $this->mergeTableDefs($currentDef, $alterDef);
0 ignored issues
show
Bug introduced by
It seems like $currentDef can also be of type null; however, parameter $tableDef of Garden\Db\Drivers\SqliteDb::mergeTableDefs() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

38
            $tableDef = $this->mergeTableDefs(/** @scrutinizer ignore-type */ $currentDef, $alterDef);
Loading history...
39
        } else {
40 1
            $tableDef = $alterDef['def'];
41
        }
42
43
        // Drop all of the indexes on the current table.
44 7
        foreach (self::val('indexes', $currentDef, []) as $indexDef) {
45 5
            if (self::val('type', $indexDef, Db::INDEX_IX) === Db::INDEX_IX) {
46 5
                $this->dropIndex($indexDef['name']);
47
            }
48
        }
49
50 7
        $tmpTable = $table.'_'.time();
51
52
        // Rename the current table.
53 7
        $this->renameTable($table, $tmpTable);
54
55
        // Create the new table.
56 7
        $this->createTableDb($tableDef, $options);
57
58
        // Figure out the columns that we can insert.
59 7
        $columns = array_keys(array_intersect_key($tableDef['columns'], $currentDef['columns']));
60
61
        // Build the insert/select statement.
62 7
        $sql = 'insert into '.$this->prefixTable($table)."\n".
63 7
            $this->bracketList($columns, '`')."\n".
64 7
            $this->buildSelect($tmpTable, [], ['columns' => $columns]);
65
66 7
        $this->queryDefine($sql);
67
68
        // Drop the temp table.
69 7
        $this->dropTable($tmpTable);
70 7
    }
71
72
    /**
73
     * Rename a table.
74
     *
75
     * @param string $old The old name of the table.
76
     * @param string $new The new name of the table.
77
     */
78 7
    private function renameTable($old, $new) {
79
        $renameSql = 'alter table '.
80 7
            $this->prefixTable($old).
81 7
            ' rename to '.
82 7
            $this->prefixTable($new);
83 7
        $this->queryDefine($renameSql);
84 7
    }
85
86
    /**
87
     * Merge a table def with its alter def so that no columns/indexes are lost in an alter.
88
     *
89
     * @param array $tableDef The table def.
90
     * @param array $alterDef The alter def.
91
     * @return array The new table def.
92
     */
93 6
    private function mergeTableDefs(array $tableDef, array $alterDef) {
94 6
        $result = $tableDef;
95
96 6
        if ($this->findPrimaryKeyIndex($alterDef['add']['indexes'])) {
97 2
            $remove = null;
98 2
            foreach ($result['indexes'] as $i => $index) {
99 2
                if ($index['type'] === Db::INDEX_PK) {
100 2
                    $remove = $i;
101
                }
102
            }
103 2
            if ($remove !== null) {
104 2
                unset($result['indexes'][$i]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $i seems to be defined by a foreach iteration on line 98. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
105
            }
106
        }
107
108 6
        $result['columns'] = array_merge($result['columns'], $alterDef['def']['columns']);
109 6
        $result['indexes'] = array_merge($result['indexes'], $alterDef['add']['indexes']);
110
111 6
        return $result;
112
    }
113
114
    /**
115
     * Drop an index.
116
     *
117
     * @param string $index The name of the index to drop.
118
     */
119 2
    protected function dropIndex($index) {
120
        $sql = 'drop index if exists '.
121 2
            $this->escape($index);
122 2
        $this->queryDefine($sql);
123 2
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 31
    protected function buildInsert($table, array $row, $options = []) {
129 31
        if (self::val(Db::OPTION_UPSERT, $options)) {
130
            throw new \Exception("Upsert is not supported.");
131 31
        } elseif (self::val(Db::OPTION_IGNORE, $options)) {
132 2
            $sql = 'insert or ignore into ';
133 30
        } elseif (self::val(Db::OPTION_REPLACE, $options)) {
134 2
            $sql = 'insert or replace into ';
135
        } else {
136 29
            $sql = 'insert into ';
137
        }
138 31
        $sql .= $this->prefixTable($table);
139
140
        // Add the list of values.
141
        $sql .=
142 31
            "\n".$this->bracketList(array_keys($row), '`').
143 31
            "\nvalues".$this->bracketList($row, "'");
144
145 31
        return $sql;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 1
    protected function buildLike(string $column, $value): string {
152 1
        return "$column like ".$this->quote($value)." escape '\\'";
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 6
    protected function buildUpdate($table, array $set, array $where, array $options = []): string {
159
        $sql = 'update '.
160 6
            (empty($options[Db::OPTION_IGNORE]) ? '' : 'or ignore ').
161 6
            $this->prefixTable($table).
162 6
            "\nset\n  ";
163
164 6
        $parts = [];
165 6
        foreach ($set as $key => $value) {
166 6
            $escapedKey = $this->escape($key);
167 6
            $parts[] = "$escapedKey = ".$this->quote($value, $escapedKey);
168
        }
169 6
        $sql .= implode(",\n  ", $parts);
170
171 6
        if (!empty($where)) {
172 6
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND);
173
        }
174
175 6
        return $sql;
176
    }
177
178
    /**
179
     * Construct a column definition string from an array defintion.
180
     *
181
     * @param string $name The name of the column.
182
     * @param array $cdef The column definition.
183
     * @return string Returns a string representing the column definition.
184
     */
185 16
    protected function columnDefString($name, array $cdef) {
186
        $cdef += [
187 16
            'autoIncrement' => false,
188
            'primary' => false,
189
            'allowNull' => false
190
        ];
191
192
        // Auto-increments MUST be of type integer.
193 16
        if ($cdef['autoIncrement']) {
194 5
            $cdef['dbtype'] = 'integer';
195
        }
196
197 16
        $result = $this->escape($name).' '.$this->nativeDbType($cdef);
198
199 16
        if ($cdef['primary'] && $cdef['autoIncrement']) {
200
//            if (val('autoincrement', $def)) {
201 5
                $result .= ' primary key autoincrement';
202 5
                $cdef['primary'] = true;
203
//            }
204
        } else {
205 16
            if (!$cdef['allowNull']) {
206 14
                $result .= ' not null';
207
            }
208
209 16
            if (isset($cdef['default'])) {
210 8
                $result .= ' default '.$this->quote($cdef['default']);
211
            }
212
        }
213
214 16
        return $result;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220 16
    protected function nativeDbType(array $type) {
221 16
        static $translations = ['bool' => 'boolean', 'byte' => 'tinyint', 'short' => 'smallint', 'long' => 'bigint'];
222
223
        // Translate the dbtype to a MySQL native type.
224 16
        if (isset($translations[$type['dbtype']])) {
225 1
            $type['dbtype'] = $translations[$type['dbtype']];
226
        }
227
228
        // Change enum into varchar.
229 16
        if ($type['dbtype'] === 'enum') {
230 1
            $type['dbtype'] = 'varchar';
231 1
            $type['maxLength'] = array_reduce(
232 1
                $type['enum'],
233 1
                function ($carry, $item) {
234 1
                    return (int)max(strlen($item), $carry);
235 1
                }, 0);
236
        }
237
238 16
        if (!empty($type['autoIncrement'])) {
239 5
            $type['dbtype'] = 'integer';
240
        }
241
242
        // Unsigned is represented differently in MySQL.
243 16
        $unsigned = !empty($type['unsigned']) && empty($type['autoIncrement']);
244 16
        unset($type['unsigned']);
245
246 16
        $dbType = static::dbType($type).($unsigned ? ' unsigned' : '');
247
248 16
        return $dbType;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 16
    protected function createTableDb(array $tableDef, array $options = []) {
255 16
        $table = $tableDef['name'];
256 16
        $parts = [];
257
258
        // Make sure the primary key columns are defined first and in order.
259 16
        $autoInc = false;
260 16
        if ($pkIndex = $this->findPrimaryKeyIndex($tableDef['indexes'])) {
261 10
            foreach ($pkIndex['columns'] as $column) {
262 10
                $cdef = $tableDef['columns'][$column];
263 10
                $parts[] = $this->columnDefString($column, $cdef);
264 10
                $autoInc |= !empty($cdef['autoIncrement']);
265 10
                unset($tableDef['columns'][$column]);
266
            }
267
        }
268
269 16
        foreach ($tableDef['columns'] as $name => $cdef) {
270 15
            $parts[] = $this->columnDefString($name, $cdef);
271
        }
272
273
        // Add the primary key index.
274 16
        if (isset($pkIndex) && !$autoInc) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $autoInc of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
275 5
            $parts[] = 'primary key '.$this->bracketList($pkIndex['columns'], '`');
276
        }
277
278 16
        $fullTableName = $this->prefixTable($table);
279 16
        $sql = "create table $fullTableName (\n  ".
280 16
            implode(",\n  ", $parts).
281 16
            "\n)";
282
283 16
        $this->queryDefine($sql);
284
285
        // Add the rest of the indexes.
286 16
        foreach (self::val('indexes', $tableDef, []) as $index) {
287 13
            if (self::val('type', $index, Db::INDEX_IX) !== Db::INDEX_PK) {
288 13
                $this->createIndex($table, $index, $options);
289
            }
290
        }
291 16
    }
292
293
    /**
294
     * Create an index.
295
     *
296
     * @param string $table The name of the table to create the index on.
297
     * @param array $indexDef The index definition.
298
     * @param array $options Additional options for the index creation.
299
     */
300 7
    public function createIndex($table, array $indexDef, $options = []) {
301
        $sql = 'create '.
302 7
            (self::val('type', $indexDef) === Db::INDEX_UNIQUE ? 'unique ' : '').
303 7
            'index '.
304 7
            (self::val(Db::OPTION_IGNORE, $options) ? 'if not exists ' : '').
305 7
            $this->buildIndexName($table, $indexDef).
306 7
            ' on '.
307 7
            $this->prefixTable($table).
308 7
            $this->bracketList($indexDef['columns'], '`');
309
310 7
        $this->queryDefine($sql);
311 7
    }
312
313
    /**
314
     * Force a value into the appropriate php type based on its Sqlite type.
315
     *
316
     * @param mixed $value The value to force.
317
     * @param string $type The sqlite type name.
318
     * @return mixed Returns $value cast to the appropriate type.
319
     */
320 3
    protected function forceType($value, $type) {
321 3
        $type = strtolower($type);
322
323 3
        if ($type === 'null') {
324
            return null;
325 3
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
326
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
327 3
            return (int)filter_var($value, FILTER_VALIDATE_INT);
328
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
329
            'numeric', 'decimal(10,5)'])) {
330
            return filter_var($value, FILTER_VALIDATE_FLOAT);
331
        } else {
332
            return (string)$value;
333
        }
334
    }
335
336
    /**
337
     * Get the columns for a table..
338
     *
339
     * @param string $table The table to get the columns for.
340
     * @return array|null Returns an array of columns.
341
     */
342 6
    protected function fetchColumnDefsDb(string $table) {
343 6
        $cdefs = $this->query('pragma table_info('.$this->prefixTable($table, false).')')->fetchAll(PDO::FETCH_ASSOC);
344 6
        if (empty($cdefs)) {
345 4
            return null;
346
        }
347
348 5
        $columns = [];
349 5
        $pk = [];
350 5
        foreach ($cdefs as $cdef) {
351 5
            $column = Db::typeDef($cdef['type']);
352 5
            if ($column === null) {
353
                throw new \Exception("Unknown type '$columnType'.", 500);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $columnType does not exist. Did you maybe mean $column?
Loading history...
354
            }
355 5
            $column['allowNull'] = !filter_var($cdef['notnull'], FILTER_VALIDATE_BOOLEAN);
356
357 5
            if ($cdef['pk']) {
358 2
                $pk[] = $cdef['name'];
359 2
                if (strcasecmp($cdef['type'], 'integer') === 0) {
360
                    $column['autoIncrement'] = true;
361
                } else {
362 2
                    $column['primary'] = true;
363
                }
364
            }
365 5
            if ($cdef['dflt_value'] !== null) {
366 3
                $column['default'] = $this->forceType($cdef['dflt_value'], $column['type']);
367
            }
368 5
            $columns[$cdef['name']] = $column;
369
        }
370
//        $tdef = ['columns' => $columns];
371
//        if (!empty($pk)) {
372
//            $tdef['indexes'][Db::INDEX_PK] = [
373
//                'columns' => $pk,
374
//                'type' => Db::INDEX_PK
375
//            ];
376
//        }
377
//        $this->tables[$table] = $tdef;
378 5
        return $columns;
379
    }
380
381
    /**
382
     * Get the indexes for a table.
383
     *
384
     * @param string $table The name of the table to get the indexes for or an empty string to get all indexes.
385
     * @return array|null
386
     */
387 5
    protected function fetchIndexesDb($table = '') {
388 5
        $indexes = [];
389
390 5
        $indexInfos = $this->query('pragma index_list('.$this->prefixTable($table).')')->fetchAll(PDO::FETCH_ASSOC);
391 5
        foreach ($indexInfos as $row) {
392 4
            $indexName = $row['name'];
393 4
            if ($row['unique']) {
394 2
                $type = Db::INDEX_UNIQUE;
395
            } else {
396 2
                $type = Db::INDEX_IX;
397
            }
398
399
            // Query the columns in the index.
400 4
            $columns = $this->query('pragma index_info('.$this->quote($indexName).')')->fetchAll(PDO::FETCH_ASSOC);
401
402
            $index = [
403 4
                'name' => $indexName,
404 4
                'columns' => array_column($columns, 'name'),
405 4
                'type' => $type
406
            ];
407 4
            $indexes[] = $index;
408
        }
409
410 5
        return $indexes;
411
    }
412
413
    /**
414
     * Get the primary or secondary keys from the given rows.
415
     *
416
     * @param string $table The name of the table.
417
     * @param array $row The row to examine.
418
     * @param bool $quick Whether or not to quickly look for <tablename>ID for the primary key.
419
     * @return array|null Returns the primary keys and values from {@link $rows} or null if the primary key isn't found.
420
     */
421 2
    private function getPKValue($table, array $row, $quick = false) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
422 2
        if ($quick && isset($row[$table.'ID'])) {
423 1
            return [$table.'ID' => $row[$table.'ID']];
424
        }
425
426 1
        $tdef = $this->fetchTableDef($table);
427 1
        $cols = [];
428 1
        foreach ($tdef['columns'] as $name => $cdef) {
429 1
            if (empty($cdef['primary'])) {
430 1
                break;
431
            }
432 1
            if (!array_key_exists($name, $row)) {
433
                return null;
434
            }
435
436 1
            $cols[$name] = $row[$name];
437
        }
438 1
        return $cols;
439
    }
440
441
    /**
442
     * Get the all of table names in the database.
443
     *
444
     * @return array Returns an array of table names.
445
     */
446 1
    protected function fetchTableNamesDb() {
447
        // Get the table names.
448 1
        $tables = $this->get(
449 1
            new Identifier('sqlite_master'),
450
            [
451 1
                'type' => 'table',
452 1
                'name' => [Db::OP_LIKE => $this->escapeLike($this->getPx()).'%']
453
            ],
454
            [
455 1
                'columns' => ['name']
456
            ]
457 1
        )->fetchAll(PDO::FETCH_COLUMN);
458
459
        // Remove internal tables.
460 1
        $tables = array_filter($tables, function ($name) {
461 1
            return substr($name, 0, 7) !== 'sqlite_';
462 1
        });
463
464 1
        return $tables;
465
    }
466
467
    /**
468
     * {@inheritdoc}
469
     */
470 11
    public function insert(string $table, array $row, array $options = []) {
471
        // Sqlite doesn't support upsert so do upserts manually.
472 11
        if (self::val(Db::OPTION_UPSERT, $options)) {
473 2
            unset($options[Db::OPTION_UPSERT]);
474
475 2
            $keys = $this->getPKValue($table, $row, true);
476 2
            if (empty($keys)) {
477
                throw new \Exception("Cannot upsert with no key.", 500);
478
            }
479
            // Try updating first.
480 2
            $updated = $this->update(
481 2
                $table,
482 2
                array_diff_key($row, $keys),
483 2
                $keys,
484 2
                $options
485
            );
486 2
            if ($updated) {
487
                // Updated.
488 2
                if (count($keys) === 1) {
489 1
                    return array_pop($keys);
490
                } else {
491 1
                    return true;
492
                }
493
            }
494
        }
495
496 11
        $result = parent::insert($table, $row, $options);
497 11
        return $result;
498
    }
499
500
    /**
501
     * Optionally quote a where value.
502
     *
503
     * @param mixed $value The value to quote.
504
     * @param string $column The name of the column being operated on.
505
     * @return string Returns the value, optionally quoted.
506
     * @internal param bool $quote Whether or not to quote the value.
507
     */
508 39
    public function quote($value, string $column = ''): string {
509 39
        if ($value instanceof Literal) {
510
            /* @var Literal $value */
511 21
            return $value->getValue($this, $column);
512 34
        } elseif (in_array(gettype($value), ['integer', 'double'])) {
513 31
            return (string)$value;
514 16
        } elseif ($value instanceof \DateTimeInterface) {
515 2
            $value = $value->format(\DateTime::RFC3339);
516 16
        } elseif ($value === true) {
517
            return '1';
518 16
        } elseif ($value === false) {
519 1
            return '0';
520
        }
521
522 16
        return $this->getPDO()->quote($value);
523
    }
524
}
525