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
![]() |
|||
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
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 ![]() |
|||
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 |