SqliteDb::nativeDbType()   A
last analyzed

Complexity

Conditions 6
Paths 32

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 15
c 0
b 0
f 0
nc 32
nop 1
dl 0
loc 29
ccs 16
cts 16
cp 1
crap 6
rs 9.2222
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 8
    protected function alterTableDb(array $alterDef, array $options = []) {
23 8
        $this->alterTableMigrate($alterDef, $options);
24 8
    }
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 8
    private function alterTableMigrate(array $alterDef, array $options = []) {
33 8
        $table = $alterDef['name'];
34 8
        $currentDef = $this->fetchTableDef($table);
35
36
        // Merge the table definitions if we aren't dropping stuff.
37 8
        if (!self::val(Db::OPTION_DROP, $options)) {
38 7
            $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 8
        foreach (self::val('indexes', $currentDef, []) as $indexDef) {
45 5
            if (self::val('type', $indexDef, Db::INDEX_IX) === Db::INDEX_IX) {
46 2
                $this->dropIndex($indexDef['name']);
47
            }
48
        }
49
50 8
        $tmpTable = $table.'_'.time();
51
52
        // Rename the current table.
53 8
        $this->renameTable($table, $tmpTable);
54
55
        // Create the new table.
56 8
        $this->createTableDb($tableDef, $options);
57
58
        // Figure out the columns that we can insert.
59 8
        $columns = array_keys(array_intersect_key($tableDef['columns'], $currentDef['columns']));
60
61
        // Build the insert/select statement.
62 8
        $sql = 'insert into '.$this->prefixTable($table)."\n".
63 8
            $this->bracketList($columns, '`')."\n".
64 8
            $this->buildSelect($tmpTable, [], ['columns' => $columns]);
65
66 8
        $this->queryDefine($sql);
67
68
        // Drop the temp table.
69 8
        $this->dropTable($tmpTable);
70 8
    }
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 8
    private function renameTable($old, $new) {
79
        $renameSql = 'alter table '.
80 8
            $this->prefixTable($old).
81 8
            ' rename to '.
82 8
            $this->prefixTable($new);
83 8
        $this->queryDefine($renameSql);
84 8
    }
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 7
    private function mergeTableDefs(array $tableDef, array $alterDef) {
94 7
        $result = $tableDef;
95
96 7
        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]);
105
            }
106
        }
107
108 7
        $result['columns'] = array_merge($result['columns'], $alterDef['def']['columns']);
109 7
        $result['indexes'] = array_merge($result['indexes'], $alterDef['add']['indexes']);
110
111 7
        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 6
    protected function buildLike(string $column, $value): string {
152 6
        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 19
    protected function columnDefString($name, array $cdef) {
186
        $cdef += [
187 19
            'autoIncrement' => false,
188
            'primary' => false,
189
            'allowNull' => false
190
        ];
191
192
        // Auto-increments MUST be of type integer.
193 19
        if ($cdef['autoIncrement']) {
194 5
            $cdef['dbtype'] = 'integer';
195
        }
196
197 19
        $result = $this->escape($name).' '.$this->nativeDbType($cdef);
198
199 19
        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 19
            if (!$cdef['allowNull']) {
206 17
                $result .= ' not null';
207
            }
208
209 19
            if (isset($cdef['default'])) {
210 9
                $result .= ' default '.$this->quote($cdef['default']);
211
            }
212
        }
213
214 19
        return $result;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220 19
    protected function nativeDbType(array $type) {
221 19
        static $translations = ['bool' => 'boolean', 'byte' => 'tinyint', 'short' => 'smallint', 'long' => 'bigint'];
222
223
        // Translate the dbtype to a MySQL native type.
224 19
        if (isset($translations[$type['dbtype']])) {
225 1
            $type['dbtype'] = $translations[$type['dbtype']];
226
        }
227
228
        // Change enum into varchar.
229 19
        if ($type['dbtype'] === 'enum') {
230 1
            $type['dbtype'] = 'varchar';
231 1
            $type['maxLength'] = array_reduce(
232 1
                $type['enum'],
233
                function ($carry, $item) {
234 1
                    return (int)max(strlen($item), $carry);
235 1
                }, 0);
236
        }
237
238 19
        if (!empty($type['autoIncrement'])) {
239 5
            $type['dbtype'] = 'integer';
240
        }
241
242
        // Unsigned is represented differently in MySQL.
243 19
        $unsigned = !empty($type['unsigned']) && empty($type['autoIncrement']);
244 19
        unset($type['unsigned']);
245
246 19
        $dbType = static::dbType($type).($unsigned ? ' unsigned' : '');
247
248 19
        return $dbType;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 19
    protected function createTableDb(array $tableDef, array $options = []) {
255 19
        $table = $tableDef['name'];
256 19
        $parts = [];
257
258
        // Make sure the primary key columns are defined first and in order.
259 19
        $autoInc = false;
260 19
        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 19
        foreach ($tableDef['columns'] as $name => $cdef) {
270 18
            $parts[] = $this->columnDefString($name, $cdef);
271
        }
272
273
        // Add the primary key index.
274 19
        if (isset($pkIndex) && !$autoInc) {
275 5
            $parts[] = 'primary key '.$this->bracketList($pkIndex['columns'], '`');
276
        }
277
278 19
        $fullTableName = $this->prefixTable($table);
279 19
        $sql = "create table $fullTableName (\n  ".
280 19
            implode(",\n  ", $parts).
281 19
            "\n)";
282
283 19
        $this->queryDefine($sql);
284
285
        // Add the rest of the indexes.
286 19
        foreach (self::val('indexes', $tableDef, []) as $index) {
287 13
            if (self::val('type', $index, Db::INDEX_IX) !== Db::INDEX_PK) {
288 7
                $this->createIndex($table, $index, $options);
289
            }
290
        }
291 19
    }
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 8
    protected function fetchColumnDefsDb(string $table) {
343 8
        $cdefs = $this->query('pragma table_info('.$this->prefixTable($table, false).')')->fetchAll(PDO::FETCH_ASSOC);
344 8
        if (empty($cdefs)) {
345 5
            return null;
346
        }
347
348 7
        $columns = [];
349 7
        $pk = [];
350 7
        foreach ($cdefs as $cdef) {
351 7
            $column = Db::typeDef($cdef['type']);
352 7
            if ($column === null) {
353
                throw new \Exception("Unknown type '$columnType'.", 500);
354
            }
355 7
            $column['allowNull'] = !filter_var($cdef['notnull'], FILTER_VALIDATE_BOOLEAN);
356
357 7
            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 7
            if ($cdef['dflt_value'] !== null) {
366 3
                $column['default'] = $this->forceType($cdef['dflt_value'], $column['type']);
367
            }
368 7
            $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 7
        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 6
    protected function fetchIndexesDb($table = '') {
388 6
        $indexes = [];
389
390 6
        $indexInfos = $this->query('pragma index_list('.$this->prefixTable($table).')')->fetchAll(PDO::FETCH_ASSOC);
391 6
        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 6
        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 6
    protected function fetchTableNamesDb() {
447
        // Get the table names.
448 6
        $tables = $this->get(
449 6
            new Identifier('sqlite_master'),
450
            [
451 6
                'type' => 'table',
452 6
                'name' => [Db::OP_LIKE => $this->escapeLike($this->getPx()).'%']
453
            ],
454
            [
455 6
                'columns' => ['name']
456
            ]
457 6
        )->fetchAll(PDO::FETCH_COLUMN);
458
459
        // Remove internal tables.
460
        $tables = array_filter($tables, function ($name) {
461 6
            return substr($name, 0, 7) !== 'sqlite_';
462 6
        });
463
464 6
        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 41
    public function quote($value, string $column = ''): string {
509 41
        if ($value instanceof Literal) {
510
            /* @var Literal $value */
511 21
            return $value->getValue($this, $column);
512 36
        } elseif (in_array(gettype($value), ['integer', 'double'])) {
513 31
            return (string)$value;
514 18
        } elseif ($value instanceof \DateTimeInterface) {
515 2
            $value = $value->format(\DateTime::RFC3339);
516 18
        } elseif ($value === true) {
517
            return '1';
518 18
        } elseif ($value === false) {
519 1
            return '0';
520
        }
521
522 18
        return $this->getPDO()->quote($value);
523
    }
524
}
525