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 | use Garden\Db\Drivers\MySqlDb; |
||
12 | use Garden\Db\Drivers\SqliteDb; |
||
13 | |||
14 | /** |
||
15 | * Defines a standard set of methods that all database drivers must conform to. |
||
16 | */ |
||
17 | abstract class Db { |
||
18 | use Utils\FetchModeTrait; |
||
19 | |||
20 | const QUERY_DEFINE = 'define'; |
||
21 | const QUERY_READ = 'read'; |
||
22 | const QUERY_WRITE = 'write'; |
||
23 | |||
24 | const INDEX_PK = 'primary'; |
||
25 | const INDEX_IX = 'index'; |
||
26 | const INDEX_UNIQUE = 'unique'; |
||
27 | |||
28 | const OPTION_REPLACE = 'replace'; |
||
29 | const OPTION_IGNORE = 'ignore'; |
||
30 | const OPTION_UPSERT = 'upsert'; |
||
31 | const OPTION_TRUNCATE = 'truncate'; |
||
32 | const OPTION_DROP = 'drop'; |
||
33 | const OPTION_FETCH_MODE = 'fetchMode'; |
||
34 | |||
35 | const OP_EQ = '='; |
||
36 | const OP_GT = '>'; |
||
37 | const OP_GTE = '>='; |
||
38 | const OP_IN = '$in'; |
||
39 | const OP_LIKE = '$like'; |
||
40 | const OP_LT = '<'; |
||
41 | const OP_LTE = '<='; |
||
42 | const OP_NEQ = '<>'; |
||
43 | |||
44 | const OP_AND = '$and'; |
||
45 | const OP_OR = '$or'; |
||
46 | |||
47 | /** |
||
48 | * @var string[] Maps PDO drivers to db classes. |
||
49 | */ |
||
50 | private static $drivers = [ |
||
51 | 'mysql' => MySqlDb::class, |
||
52 | 'sqlite' => SqliteDb::class |
||
53 | ]; |
||
54 | |||
55 | /** |
||
56 | * @var array The canonical database types. |
||
57 | */ |
||
58 | private static $types = [ |
||
59 | // String |
||
60 | 'char' => ['type' => 'string', 'length' => true], |
||
61 | 'varchar' => ['type' => 'string', 'length' => true], |
||
62 | 'tinytext' => ['type' => 'string', 'schema' => ['maxLength' => 255]], |
||
63 | 'text' => ['type' => 'string', 'schema' => ['maxLength' => 65535]], |
||
64 | 'mediumtext' => ['type' => 'string', 'schema' => ['maxLength' => 16777215]], |
||
65 | 'longtext' => ['type' => 'string', 'schema' => ['maxLength' => 4294967295]], |
||
66 | 'binary' => ['type' => 'string', 'length' => true], |
||
67 | 'varbinary' => ['type' => 'string', 'length' => true], |
||
68 | |||
69 | // Boolean |
||
70 | 'bool' => ['type' => 'boolean'], |
||
71 | |||
72 | // Integer |
||
73 | 'byte' => ['type' => 'integer', 'schema' => ['maximum' => 127, 'minimum' => -128]], |
||
74 | 'short' => ['type' => 'integer', 'schema' => ['maximum' => 32767, 'minimum' => -32768]], |
||
75 | 'int' => ['type' => 'integer', 'schema' => ['maximum' => 2147483647, 'minimum' => -2147483648]], |
||
76 | 'long' => ['type' => 'integer'], |
||
77 | |||
78 | // Number |
||
79 | 'float' => ['type' => 'number'], |
||
80 | 'double' => ['type' => 'number'], |
||
81 | 'decimal' => ['type' => 'number', 'precision' => true], |
||
82 | 'numeric' => ['type' => 'number', 'precision' => true], |
||
83 | |||
84 | // Date/Time |
||
85 | 'datetime' => ['type' => 'datetime'], |
||
86 | 'timestamp' => ['type' => 'datetime'], |
||
87 | 'date' => ['type' => 'datetime'], |
||
88 | |||
89 | // Enum |
||
90 | 'enum' => ['type' => 'string', 'enum' => true], |
||
91 | |||
92 | // Schema types |
||
93 | 'string' => 'varchar', |
||
94 | 'boolean' => 'bool', |
||
95 | 'integer' => 'int', |
||
96 | 'number' => 'float', |
||
97 | |||
98 | // Other aliases |
||
99 | 'character' => 'char', |
||
100 | 'tinyint' => 'byte', |
||
101 | 'int8' => 'byte', |
||
102 | 'smallint' => 'short', |
||
103 | 'int16' => 'short', |
||
104 | 'int32' => 'int', |
||
105 | 'bigint' => 'long', |
||
106 | 'int64' => 'long', |
||
107 | 'real' => 'double', |
||
108 | ]; |
||
109 | |||
110 | /** |
||
111 | * @var string The database prefix. |
||
112 | */ |
||
113 | private $px = ''; |
||
114 | |||
115 | /** |
||
116 | * @var array A cached copy of the table schemas indexed by lowercase name. |
||
117 | */ |
||
118 | private $tables = []; |
||
119 | |||
120 | /** |
||
121 | * @var array|null A cached copy of the table names indexed by lowercase name. |
||
122 | */ |
||
123 | private $tableNames = null; |
||
124 | |||
125 | /** |
||
126 | * @var \PDO |
||
127 | */ |
||
128 | private $pdo; |
||
129 | |||
130 | /** |
||
131 | * Initialize an instance of the {@link MySqlDb} class. |
||
132 | * |
||
133 | * @param PDO $pdo The connection to the database. |
||
134 | * @param string $px The database prefix. |
||
135 | */ |
||
136 | public function __construct(PDO $pdo, string $px = '') { |
||
137 | $this->pdo = $pdo; |
||
138 | $this->px = $px; |
||
139 | |||
140 | $fetchMode = $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE); |
||
141 | $this->setFetchMode(in_array($fetchMode, [0, PDO::FETCH_BOTH], true) ? PDO::FETCH_ASSOC: $fetchMode); |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Get the name of the class that handles a database driver. |
||
146 | * |
||
147 | * @param string|PDO $driver The name of the driver or a database connection. |
||
148 | * @return null|string Returns the driver classname or **null** if one isn't found. |
||
149 | */ |
||
150 | public static function driverClass($driver) { |
||
151 | if ($driver instanceof PDO) { |
||
152 | $name = $driver->getAttribute(PDO::ATTR_DRIVER_NAME); |
||
153 | } else { |
||
154 | $name = (string)$driver; |
||
155 | } |
||
156 | |||
157 | $name = strtolower($name); |
||
158 | return isset(self::$drivers[$name]) ? self::$drivers[$name] : null; |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * Add a table to the database. |
||
163 | * |
||
164 | * @param array $tableDef The table definition. |
||
165 | * @param array $options An array of additional options when adding the table. |
||
166 | */ |
||
167 | abstract protected function createTableDb(array $tableDef, array $options = []); |
||
168 | |||
169 | /** |
||
170 | * Alter a table in the database. |
||
171 | * |
||
172 | * When altering a table you pass an array with three optional keys: add, drop, and alter. |
||
173 | * Each value is consists of a table definition in a format that would be passed to {@link Db::setTableDef()}. |
||
174 | * |
||
175 | * @param array $alterDef The alter definition. |
||
176 | * @param array $options An array of additional options when adding the table. |
||
177 | */ |
||
178 | abstract protected function alterTableDb(array $alterDef, array $options = []); |
||
179 | |||
180 | /** |
||
181 | * Drop a table. |
||
182 | * |
||
183 | * @param string $table The name of the table to drop. |
||
184 | * @param array $options An array of additional options when adding the table. |
||
185 | 10 | */ |
|
186 | 10 | final public function dropTable(string $table, array $options = []) { |
|
187 | 10 | $options += [Db::OPTION_IGNORE => false]; |
|
188 | $this->dropTableDb($table, $options); |
||
189 | 10 | ||
190 | 10 | $tableKey = strtolower($table); |
|
191 | 10 | unset($this->tables[$tableKey], $this->tableNames[$tableKey]); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * Perform the actual table drop. |
||
196 | * |
||
197 | * @param string $table The name of the table to drop. |
||
198 | * @param array $options An array of additional options when adding the table. |
||
199 | */ |
||
200 | abstract protected function dropTableDb(string $table, array $options = []); |
||
201 | |||
202 | /** |
||
203 | * Get the names of all the tables in the database. |
||
204 | * |
||
205 | * @return string[] Returns an array of table names without prefixes. |
||
206 | 12 | */ |
|
207 | 12 | final public function fetchTableNames() { |
|
208 | 2 | if ($this->tableNames !== null) { |
|
209 | return array_values($this->tableNames); |
||
210 | } |
||
211 | 12 | ||
212 | $names = $this->fetchTableNamesDb(); |
||
213 | 12 | ||
214 | 12 | $this->tableNames = []; |
|
215 | 12 | foreach ($names as $name) { |
|
216 | 12 | $name = $this->stripPrefix($name); |
|
217 | $this->tableNames[strtolower($name)] = $name; |
||
218 | } |
||
219 | 12 | ||
220 | return array_values($this->tableNames); |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Fetch the table names from the underlying database layer. |
||
225 | * |
||
226 | * The driver should return all table names. It doesn't have to strip the prefix. |
||
227 | * |
||
228 | * @return string[] |
||
229 | */ |
||
230 | abstract protected function fetchTableNamesDb(); |
||
231 | |||
232 | /** |
||
233 | * Get a table definition. |
||
234 | * |
||
235 | * @param string $table The name of the table. |
||
236 | * @return array|null Returns the table definition or null if the table does not exist. |
||
237 | 80 | */ |
|
238 | 80 | final public function fetchTableDef(string $table) { |
|
239 | $tableKey = strtolower($table); |
||
240 | |||
241 | 80 | // First check the table cache. |
|
242 | 57 | if (isset($this->tables[$tableKey])) { |
|
243 | $tableDef = $this->tables[$tableKey]; |
||
244 | 57 | ||
245 | 57 | if (isset($tableDef['columns'], $tableDef['indexes'])) { |
|
246 | return $tableDef; |
||
247 | 44 | } |
|
248 | 36 | } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) { |
|
249 | return null; |
||
250 | } |
||
251 | 14 | ||
252 | 14 | $tableDef = $this->fetchTableDefDb($table); |
|
253 | 12 | if ($tableDef !== null) { |
|
254 | 12 | $this->fixIndexes($tableDef['name'], $tableDef); |
|
255 | $this->tables[$tableKey] = $tableDef; |
||
256 | } |
||
257 | 14 | ||
258 | return $tableDef; |
||
259 | } |
||
260 | |||
261 | /** |
||
262 | * Fetch the table definition from the database. |
||
263 | * |
||
264 | * @param string $table The name of the table to get. |
||
265 | * @return array|null Returns the table def or **null** if the table doesn't exist. |
||
266 | */ |
||
267 | abstract protected function fetchTableDefDb(string $table); |
||
268 | |||
269 | |||
270 | /** |
||
271 | * Get the column definitions for a table. |
||
272 | * |
||
273 | * @param string $table The name of the table to get the columns for. |
||
274 | * @return array|null Returns an array of column definitions. |
||
275 | 3 | */ |
|
276 | 3 | final public function fetchColumnDefs(string $table) { |
|
277 | $tableKey = strtolower($table); |
||
278 | 3 | ||
279 | 1 | if (!empty($this->tables[$tableKey]['columns'])) { |
|
280 | 2 | $this->tables[$tableKey]['columns']; |
|
281 | } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) { |
||
282 | return null; |
||
283 | } |
||
284 | 3 | ||
285 | 3 | $columnDefs = $this->fetchColumnDefsDb($table); |
|
286 | 3 | if ($columnDefs !== null) { |
|
287 | $this->tables[$tableKey]['columns'] = $columnDefs; |
||
288 | 3 | } |
|
289 | return $columnDefs; |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * Get the column definitions from the database. |
||
294 | * |
||
295 | * @param string $table The name of the table to fetch the columns for. |
||
296 | * @return array|null |
||
297 | */ |
||
298 | abstract protected function fetchColumnDefsDb(string $table); |
||
299 | |||
300 | /** |
||
301 | * Get the canonical type based on a type string. |
||
302 | * |
||
303 | * @param string $type A type string. |
||
304 | * @return array|null Returns the type schema array or **null** if a type isn't found. |
||
305 | 65 | */ |
|
306 | public static function typeDef(string $type) { |
||
307 | 65 | // Check for the unsigned signifier. |
|
308 | 65 | $unsigned = null; |
|
309 | 6 | if ($type[0] === 'u') { |
|
310 | 6 | $unsigned = true; |
|
311 | 63 | $type = substr($type, 1); |
|
312 | 2 | } elseif (preg_match('`(.+)\s+unsigned`i', $type, $m)) { |
|
313 | 2 | $unsigned = true; |
|
314 | $type = $m[1]; |
||
315 | } |
||
316 | |||
317 | 65 | // Remove brackets from the type. |
|
318 | 65 | $brackets = null; |
|
319 | 41 | if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) { |
|
320 | 41 | $brackets = $m[2]; |
|
321 | $type = $m[1]; |
||
322 | } |
||
323 | |||
324 | 65 | // Look for the type. |
|
325 | 65 | $type = strtolower($type); |
|
326 | 65 | if (isset(self::$types[$type])) { |
|
327 | 65 | $row = self::$types[$type]; |
|
328 | $dbtype = $type; |
||
329 | |||
330 | 65 | // Resolve an alias. |
|
331 | 2 | if (is_string($row)) { |
|
332 | 65 | $dbtype = $row; |
|
333 | $row = self::$types[$row]; |
||
334 | } |
||
335 | } else { |
||
336 | return null; |
||
337 | } |
||
338 | |||
339 | // Now that we have a type row we can build a schema for it. |
||
340 | 65 | $schema = [ |
|
341 | 65 | 'type' => $row['type'], |
|
342 | 'dbtype' => $dbtype |
||
343 | ]; |
||
344 | 65 | ||
345 | 44 | if (!empty($row['schema'])) { |
|
346 | $schema += $row['schema']; |
||
347 | } |
||
348 | 65 | ||
349 | 6 | if ($row['type'] === 'integer' && $unsigned) { |
|
350 | $schema['unsigned'] = true; |
||
351 | 6 | ||
352 | 6 | if (!empty($schema['maximum'])) { |
|
353 | 6 | $schema['maximum'] = $schema['maximum'] * 2 + 1; |
|
354 | $schema['minimum'] = 0; |
||
355 | } |
||
356 | } |
||
357 | 65 | ||
358 | 32 | if (!empty($row['length'])) { |
|
359 | $schema['maxLength'] = (int)$brackets ?: 255; |
||
360 | } |
||
361 | 65 | ||
362 | 2 | if (!empty($row['precision'])) { |
|
363 | 2 | $parts = array_map('trim', explode(',', $brackets)); |
|
364 | 2 | $schema['precision'] = (int)$parts[0]; |
|
365 | 2 | if (isset($parts[1])) { |
|
366 | $schema['scale'] = (int)$parts[1]; |
||
367 | } |
||
368 | } |
||
369 | 65 | ||
370 | 3 | if (!empty($row['enum'])) { |
|
371 | $enum = explode(',', $brackets); |
||
372 | 3 | $schema['enum'] = array_map(function ($str) { |
|
373 | 3 | return trim($str, "'\" \t\n\r\0\x0B"); |
|
374 | }, $enum); |
||
375 | } |
||
376 | 65 | ||
377 | return $schema; |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * Get the database type string from a type definition. |
||
382 | * |
||
383 | * This is the opposite of {@link Db::typeDef()}. |
||
384 | * |
||
385 | * @param array $typeDef The type definition array. |
||
386 | * @return string Returns a db type string. |
||
387 | 44 | */ |
|
388 | 44 | protected static function dbType(array $typeDef) { |
|
389 | $dbtype = $typeDef['dbtype']; |
||
390 | 44 | ||
391 | 22 | if (!empty($typeDef['maxLength'])) { |
|
392 | 37 | $dbtype .= "({$typeDef['maxLength']})"; |
|
393 | } elseif (!empty($typeDef['unsigned'])) { |
||
394 | 37 | $dbtype = 'u'.$dbtype; |
|
395 | } elseif (!empty($typeDef['precision'])) { |
||
396 | $dbtype .= "({$typeDef['precision']}"; |
||
397 | if (!empty($typeDef['scale'])) { |
||
398 | $dbtype .= ",{$typeDef['scale']}"; |
||
399 | } |
||
400 | 37 | $dbtype .= ')'; |
|
401 | } elseif (!empty($typeDef['enum'])) { |
||
402 | 1 | $parts = array_map(function ($str) { |
|
403 | 1 | return "'{$str}'"; |
|
404 | 1 | }, $typeDef['enum']); |
|
405 | $dbtype .= '('.implode(',', $parts).')'; |
||
406 | 44 | } |
|
407 | return $dbtype; |
||
408 | } |
||
409 | |||
410 | |||
411 | /** |
||
412 | * Get the native database type based on a type schema. |
||
413 | * |
||
414 | * The default implementation of this method returns the canonical db types. Individual database classes will have |
||
415 | * to override to provide any differences. |
||
416 | * |
||
417 | * @param array $type The type schema. |
||
418 | * @return string |
||
419 | */ |
||
420 | abstract protected function nativeDbType(array $type); |
||
421 | |||
422 | /** |
||
423 | * Set a table definition to the database. |
||
424 | * |
||
425 | * @param array $tableDef The table definition. |
||
426 | * @param array $options An array of additional options when adding the table. |
||
427 | * @throws \Exception Throws an exception if there is a mismatch in the primary key column and index. |
||
428 | 80 | */ |
|
429 | 80 | final public function defineTable(array $tableDef, array $options = []) { |
|
430 | $options += [Db::OPTION_DROP => false]; |
||
431 | 80 | ||
432 | 80 | $tableName = $tableDef['name']; |
|
433 | 80 | $tableKey = strtolower($tableName); |
|
434 | 80 | $tableDef['name'] = $tableName; |
|
435 | $curTable = $this->fetchTableDef($tableName); |
||
436 | 80 | ||
437 | $this->fixIndexes($tableName, $tableDef, $curTable); |
||
438 | 80 | ||
439 | if ($this->tableNames === null) { |
||
440 | 12 | // Fetch all tables here now so the cache knows all tables that exist. |
|
441 | $this->fetchTableNames(); |
||
442 | } |
||
443 | 80 | ||
444 | 44 | if (!$curTable) { |
|
445 | 44 | $this->createTableDb($tableDef, $options); |
|
446 | 44 | $this->tables[$tableKey] = $tableDef; |
|
447 | 44 | $this->tableNames[$tableKey] = $tableDef['name']; |
|
448 | return; |
||
449 | } |
||
450 | 55 | // This is the alter statement. |
|
451 | $alterDef = ['name' => $tableName]; |
||
452 | |||
453 | 55 | // Figure out the columns that have changed. |
|
454 | 55 | $curColumns = (array)$curTable['columns']; |
|
455 | $newColumns = (array)$tableDef['columns']; |
||
456 | 55 | ||
457 | $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns); |
||
458 | 55 | $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) { |
|
459 | 55 | $search = ['dbtype', 'allowNull', 'default', 'maxLength']; |
|
460 | 55 | foreach ($search as $key) { |
|
461 | if (self::val($key, $curr) !== self::val($key, $new)) { |
||
462 | 8 | // Return 0 if the values are different, not the same. |
|
463 | return 0; |
||
464 | } |
||
465 | } |
||
466 | |||
467 | 53 | // Enum checking. |
|
468 | if (isset($curr['enum']) xor isset($new['enum'])) { |
||
469 | 53 | return 0; |
|
470 | } elseif (isset($curr['enum']) && isset($new['enum']) |
||
471 | 2 | && ( |
|
472 | 53 | count($curr['enum']) !== count($new['enum']) |
|
473 | || !empty(array_diff($curr['enum'], $new['enum'])) |
||
474 | ) |
||
475 | 2 | ) { |
|
476 | return 0; |
||
477 | } |
||
478 | 51 | ||
479 | 55 | return 1; |
|
480 | }); |
||
481 | |||
482 | 55 | // Figure out the indexes that have changed. |
|
483 | 55 | $curIndexes = (array)self::val('indexes', $curTable, []); |
|
484 | $newIndexes = (array)self::val('indexes', $tableDef, []); |
||
485 | 55 | ||
486 | $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']); |
||
487 | 55 | ||
488 | 55 | $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']); |
|
489 | 2 | if ($options[Db::OPTION_DROP]) { |
|
490 | 2 | $alterDef['drop']['columns'] = array_diff_key($curColumns, $newColumns); |
|
491 | $alterDef['drop']['indexes'] = $dropIndexes; |
||
492 | 53 | } else { |
|
493 | 53 | $alterDef['drop']['columns'] = []; |
|
494 | $alterDef['drop']['indexes'] = []; |
||
495 | |||
496 | 53 | // If the primary key has changed then the old one needs to be dropped. |
|
497 | 4 | if ($pk = $this->findPrimaryKeyIndex($dropIndexes)) { |
|
498 | $alterDef['drop']['indexes'][] = $pk; |
||
499 | } |
||
500 | } |
||
501 | |||
502 | 55 | // Check to see if any alterations at all need to be made. |
|
503 | 55 | if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) && |
|
504 | 55 | empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) && |
|
505 | empty($alterDef['alter']['columns']) |
||
506 | 39 | ) { |
|
507 | return; |
||
508 | } |
||
509 | 16 | ||
510 | $alterDef['def'] = $tableDef; |
||
511 | |||
512 | 16 | // Alter the table. |
|
513 | $this->alterTableDb($alterDef, $options); |
||
514 | |||
515 | 16 | // Update the cached schema. |
|
516 | 16 | $tableDef['name'] = $tableName; |
|
517 | $this->tables[$tableKey] = $tableDef; |
||
518 | 16 | ||
519 | 16 | $this->tableNames[$tableKey] = $tableName; |
|
520 | } |
||
521 | |||
522 | /** |
||
523 | * Find the primary key in an array of indexes. |
||
524 | * |
||
525 | * @param array $indexes The indexes to search. |
||
526 | * @return array|null Returns the primary key or **null** if there isn't one. |
||
527 | 64 | */ |
|
528 | 64 | protected function findPrimaryKeyIndex(array $indexes) { |
|
529 | 15 | foreach ($indexes as $index) { |
|
530 | 12 | if ($index['type'] === Db::INDEX_PK) { |
|
531 | return $index; |
||
532 | } |
||
533 | 53 | } |
|
534 | return null; |
||
535 | } |
||
536 | |||
537 | /** |
||
538 | * Move the primary key index into the correct place for database drivers. |
||
539 | * |
||
540 | * @param string $tableName The name of the table. |
||
541 | * @param array &$tableDef The table definition. |
||
542 | * @param array|null $curTableDef The current database table def used to resolve conflicts in some names. |
||
543 | * @throws \Exception Throws an exception when there is a mismatch between the primary index and the primary key |
||
544 | * defined on the columns themselves. |
||
545 | 80 | */ |
|
546 | 80 | private function fixIndexes(string $tableName, array &$tableDef, $curTableDef = null) { |
|
547 | $tableDef += ['indexes' => []]; |
||
548 | |||
549 | 80 | // Loop through the columns and add the primary key index. |
|
550 | 80 | $primaryColumns = []; |
|
551 | 80 | foreach ($tableDef['columns'] as $cname => $cdef) { |
|
552 | 28 | if (!empty($cdef['primary'])) { |
|
553 | $primaryColumns[] = $cname; |
||
554 | } |
||
555 | } |
||
556 | |||
557 | 80 | // Massage the primary key index. |
|
558 | 80 | $primaryFound = false; |
|
559 | 68 | foreach ($tableDef['indexes'] as &$indexDef) { |
|
560 | $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null]; |
||
561 | 68 | ||
562 | 32 | if ($indexDef['type'] === Db::INDEX_PK) { |
|
563 | $primaryFound = true; |
||
564 | 32 | ||
565 | 10 | if (empty($primaryColumns)) { |
|
566 | 10 | foreach ($indexDef['columns'] as $cname) { |
|
567 | $tableDef['columns'][$cname]['primary'] = true; |
||
568 | 24 | } |
|
569 | 32 | } elseif (array_diff($primaryColumns, $indexDef['columns'])) { |
|
570 | throw new \Exception("There is a mismatch in the primary key index and primary key columns.", 500); |
||
571 | 58 | } |
|
572 | 41 | } elseif (isset($curTableDef['indexes'])) { |
|
573 | 41 | foreach ($curTableDef['indexes'] as $curIndexDef) { |
|
574 | 41 | if ($this->indexCompare($indexDef, $curIndexDef) === 0) { |
|
575 | 41 | if (!empty($curIndexDef['name'])) { |
|
576 | $indexDef['name'] = $curIndexDef['name']; |
||
577 | 41 | } |
|
578 | break; |
||
579 | } |
||
580 | } |
||
581 | } |
||
582 | } |
||
583 | 80 | ||
584 | 4 | if (!$primaryFound && !empty($primaryColumns)) { |
|
585 | 4 | $tableDef['indexes'][] = [ |
|
586 | 'columns' => $primaryColumns, |
||
587 | 'type' => Db::INDEX_PK |
||
588 | ]; |
||
589 | 80 | } |
|
590 | } |
||
591 | |||
592 | /** |
||
593 | * Get the database prefix. |
||
594 | * |
||
595 | * @return string Returns the current db prefix. |
||
596 | 12 | */ |
|
597 | 12 | public function getPx(): string { |
|
598 | return $this->px; |
||
599 | } |
||
600 | |||
601 | /** |
||
602 | * Set the database prefix. |
||
603 | * |
||
604 | * @param string $px The new database prefix. |
||
605 | */ |
||
606 | public function setPx(string $px) { |
||
607 | $this->px = $px; |
||
608 | } |
||
609 | |||
610 | /** |
||
611 | * Compare two index definitions to see if they have the same columns and same type. |
||
612 | * |
||
613 | * @param array $a The first index. |
||
614 | * @param array $b The second index. |
||
615 | * @return int Returns an integer less than, equal to, or greater than zero if {@link $a} is |
||
616 | * considered to be respectively less than, equal to, or greater than {@link $b}. |
||
617 | 47 | */ |
|
618 | 47 | private function indexCompare(array $a, array $b): int { |
|
619 | 15 | if ($a['columns'] > $b['columns']) { |
|
620 | 47 | return 1; |
|
621 | 15 | } elseif ($a['columns'] < $b['columns']) { |
|
622 | return -1; |
||
623 | } |
||
624 | 43 | ||
625 | 43 | return strcmp( |
|
626 | 43 | isset($a['type']) ? $a['type'] : '', |
|
627 | isset($b['type']) ? $b['type'] : '' |
||
628 | ); |
||
629 | } |
||
630 | |||
631 | /** |
||
632 | * Get data from the database. |
||
633 | * |
||
634 | * @param string|Identifier $table The name of the table to get the data from. |
||
635 | * @param array $where An array of where conditions. |
||
636 | * @param array $options An array of additional options. |
||
637 | * @return \PDOStatement Returns the result set. |
||
638 | */ |
||
639 | abstract public function get($table, array $where, array $options = []): \PDOStatement; |
||
640 | |||
641 | /** |
||
642 | * Get a single row from the database. |
||
643 | * |
||
644 | * This is a convenience method that calls {@link Db::get()} and shifts off the first row. |
||
645 | * |
||
646 | * @param string|Identifier $table The name of the table to get the data from. |
||
647 | * @param array $where An array of where conditions. |
||
648 | * @param array $options An array of additional options. |
||
649 | * @return array|object|null Returns the row or false if there is no row. |
||
650 | 20 | */ |
|
651 | 20 | final public function getOne($table, array $where, array $options = []) { |
|
652 | 20 | $rows = $this->get($table, $where, $options); |
|
653 | $row = $rows->fetch(); |
||
654 | 20 | ||
655 | return $row === false ? null : $row; |
||
656 | } |
||
657 | |||
658 | /** |
||
659 | * Insert a row into a table. |
||
660 | * |
||
661 | * @param string $table The name of the table to insert into. |
||
662 | * @param array $row The row of data to insert. |
||
663 | * @param array $options An array of options for the insert. |
||
664 | * |
||
665 | * Db::OPTION_IGNORE |
||
666 | * : Whether or not to ignore inserts that lead to a duplicate key. *default false* |
||
667 | * Db::OPTION_REPLACE |
||
668 | * : Whether or not to replace duplicate keys. *default false* |
||
669 | * Db::OPTION_UPSERT |
||
670 | * : Whether or not to update the existing data when duplicate keys exist. |
||
671 | * |
||
672 | * @return mixed Returns the id of the inserted record, **true** if the table doesn't have an auto increment, or **false** otherwise. |
||
673 | * @see Db::load() |
||
674 | */ |
||
675 | abstract public function insert(string $table, array $row, array $options = []); |
||
676 | |||
677 | /** |
||
678 | * Load many rows into a table. |
||
679 | * |
||
680 | * @param string $table The name of the table to insert into. |
||
681 | * @param \Traversable|array $rows A dataset to insert. |
||
682 | * Note that all rows must contain the same columns. |
||
683 | * The first row will be looked at for the structure of the insert and the rest of the rows will use this structure. |
||
684 | * @param array $options An array of options for the inserts. See {@link Db::insert()} for details. |
||
685 | * @see Db::insert() |
||
686 | */ |
||
687 | public function load(string $table, $rows, array $options = []) { |
||
688 | foreach ($rows as $row) { |
||
689 | $this->insert($table, $row, $options); |
||
690 | } |
||
691 | } |
||
692 | |||
693 | |||
694 | /** |
||
695 | * Update a row or rows in a table. |
||
696 | * |
||
697 | * @param string $table The name of the table to update. |
||
698 | * @param array $set The values to set. |
||
699 | * @param array $where The where filter for the update. |
||
700 | * @param array $options An array of options for the update. |
||
701 | * @return int Returns the number of affected rows. |
||
702 | */ |
||
703 | abstract public function update(string $table, array $set, array $where, array $options = []): int; |
||
704 | |||
705 | /** |
||
706 | * Delete rows from a table. |
||
707 | * |
||
708 | * @param string $table The name of the table to delete from. |
||
709 | * @param array $where The where filter of the delete. |
||
710 | * @param array $options An array of options. |
||
711 | * |
||
712 | * Db:OPTION_TRUNCATE |
||
713 | * : Truncate the table instead of deleting rows. In this case {@link $where} must be blank. |
||
714 | * @return int Returns the number of affected rows. |
||
715 | */ |
||
716 | abstract public function delete(string $table, array $where, array $options = []): int; |
||
717 | |||
718 | /** |
||
719 | * Reset the internal table definition cache. |
||
720 | * |
||
721 | * @return $this |
||
722 | 14 | */ |
|
723 | 14 | public function reset() { |
|
724 | 14 | $this->tables = []; |
|
725 | 14 | $this->tableNames = null; |
|
726 | return $this; |
||
727 | } |
||
728 | |||
729 | /** |
||
730 | * Build a standardized index name from an index definition. |
||
731 | * |
||
732 | * @param string $tableName The name of the table the index is in. |
||
733 | * @param array $indexDef The index definition. |
||
734 | * @return string Returns the index name. |
||
735 | 68 | */ |
|
736 | 68 | protected function buildIndexName(string $tableName, array $indexDef): string { |
|
737 | $indexDef += ['type' => Db::INDEX_IX, 'suffix' => '']; |
||
738 | 68 | ||
739 | $type = $indexDef['type']; |
||
740 | 68 | ||
741 | 33 | if ($type === Db::INDEX_PK) { |
|
742 | return 'primary'; |
||
743 | 58 | } |
|
744 | 58 | $px = self::val($type, [Db::INDEX_IX => 'ix_', Db::INDEX_UNIQUE => 'ux_'], 'ix_'); |
|
745 | 58 | $sx = $indexDef['suffix']; |
|
746 | 58 | $result = $px.$tableName.'_'.($sx ?: implode('', $indexDef['columns'])); |
|
747 | return $result; |
||
748 | } |
||
749 | |||
750 | /** |
||
751 | * Execute a query that fetches data. |
||
752 | * |
||
753 | * @param string $sql The query to execute. |
||
754 | * @param array $params Input parameters for the query. |
||
755 | * @param array $options Additional options. |
||
756 | * @return \PDOStatement Returns the result of the query. |
||
757 | * @throws \PDOException Throws an exception if something went wrong during the query. |
||
758 | 107 | */ |
|
759 | protected function query(string $sql, array $params = [], array $options = []): \PDOStatement { |
||
760 | 107 | $options += [ |
|
761 | Db::OPTION_FETCH_MODE => $this->getFetchArgs() |
||
762 | ]; |
||
763 | 107 | ||
764 | $stm = $this->getPDO()->prepare($sql); |
||
765 | |||
766 | 107 | ||
767 | 92 | if ($options[Db::OPTION_FETCH_MODE]) { |
|
768 | $stm->setFetchMode(...(array)$options[Db::OPTION_FETCH_MODE]); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
769 | } |
||
770 | 107 | ||
771 | $r = $stm->execute($params); |
||
772 | |||
773 | 107 | // This is a kludge for those that don't have errors turning into exceptions. |
|
774 | if ($r === false) { |
||
775 | list($state, $code, $msg) = $stm->errorInfo(); |
||
776 | throw new \PDOException($msg, $code); |
||
777 | } |
||
778 | 107 | ||
779 | return $stm; |
||
780 | } |
||
781 | |||
782 | /** |
||
783 | * Query the database and return a row count. |
||
784 | * |
||
785 | * @param string $sql The query to execute. |
||
786 | * @param array $params Input parameters for the query. |
||
787 | * @param array $options Additional options. |
||
788 | * @return int |
||
789 | 40 | */ |
|
790 | 40 | protected function queryModify(string $sql, array $params = [], array $options = []): int { |
|
791 | 40 | $options += [Db::OPTION_FETCH_MODE => 0]; |
|
792 | 40 | $stm = $this->query($sql, $params, $options); |
|
793 | return $stm->rowCount(); |
||
794 | } |
||
795 | |||
796 | /** |
||
797 | * Query the database and return the ID of the record that was inserted. |
||
798 | * |
||
799 | * @param string $sql The query to execute. |
||
800 | * @param array $params Input parameters for the query. |
||
801 | * @param array $options Additional options. |
||
802 | * @return mixed Returns the record ID. |
||
803 | 23 | */ |
|
804 | 23 | protected function queryID(string $sql, array $params = [], array $options = []) { |
|
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 ![]() |
|||
805 | 23 | $options += [Db::OPTION_FETCH_MODE => 0]; |
|
806 | 23 | $this->query($sql, $params, $options); |
|
807 | $r = $this->getPDO()->lastInsertId(); |
||
808 | 23 | ||
809 | return is_numeric($r) ? (int)$r : $r; |
||
810 | } |
||
811 | |||
812 | /** |
||
813 | * Query the database for a database define. |
||
814 | * |
||
815 | * @param string $sql The query to execute. |
||
816 | * @param array $options Additional options. |
||
817 | 44 | */ |
|
818 | 44 | protected function queryDefine(string $sql, array $options = []) { |
|
819 | 44 | $options += [Db::OPTION_FETCH_MODE => 0]; |
|
820 | 44 | $this->query($sql, [], $options); |
|
821 | } |
||
822 | |||
823 | /** |
||
824 | * Safely get a value out of an array. |
||
825 | * |
||
826 | * This function will always return a value even if the array key doesn't exist. |
||
827 | * The self::val() function is one of the biggest workhorses of Vanilla and shows up a lot throughout other code. |
||
828 | * It's much preferable to use this function if your not sure whether or not an array key exists rather than |
||
829 | * using @ error suppression. |
||
830 | * |
||
831 | * This function uses optimizations found in the [facebook libphputil library](https://github.com/facebook/libphutil). |
||
832 | * |
||
833 | * @param string|int $key The array key. |
||
834 | * @param array|object $array The array to get the value from. |
||
835 | * @param mixed $default The default value to return if the key doesn't exist. |
||
836 | * @return mixed The item from the array or `$default` if the array key doesn't exist. |
||
837 | * @category Array Functions |
||
838 | 98 | */ |
|
839 | 98 | protected static function val($key, $array, $default = null) { |
|
840 | if (is_array($array)) { |
||
841 | 98 | // isset() is a micro-optimization - it is fast but fails for null values. |
|
842 | 91 | if (isset($array[$key])) { |
|
843 | return $array[$key]; |
||
844 | } |
||
845 | |||
846 | 97 | // Comparing $default is also a micro-optimization. |
|
847 | 97 | if ($default === null || array_key_exists($key, $array)) { |
|
848 | return null; |
||
849 | } |
||
850 | } elseif (is_object($array)) { |
||
851 | if (isset($array->$key)) { |
||
852 | return $array->$key; |
||
853 | } |
||
854 | |||
855 | if ($default === null || property_exists($array, $key)) { |
||
856 | return null; |
||
857 | } |
||
858 | } |
||
859 | 4 | ||
860 | return $default; |
||
861 | } |
||
862 | |||
863 | /** |
||
864 | * Escape an identifier. |
||
865 | * |
||
866 | * @param string|Literal $identifier The identifier to escape. |
||
867 | * @return string Returns the field properly escaped. |
||
868 | 107 | */ |
|
869 | 107 | public function escape($identifier): string { |
|
870 | 6 | if ($identifier instanceof Literal) { |
|
871 | return $identifier->getValue($this); |
||
872 | 107 | } |
|
873 | return '`'.str_replace('`', '``', $identifier).'`'; |
||
874 | } |
||
875 | |||
876 | /** |
||
877 | * Escape a a like string so that none of its characters work as wildcards. |
||
878 | * |
||
879 | * @param string $str The string to escape. |
||
880 | * @return string Returns an escaped string. |
||
881 | 12 | */ |
|
882 | 12 | protected function escapeLike(string $str): string { |
|
883 | return addcslashes($str, '_%'); |
||
884 | } |
||
885 | |||
886 | /** |
||
887 | * Prefix a table name. |
||
888 | * |
||
889 | * @param string|Identifier $table The name of the table to prefix. |
||
890 | * @param bool $escape Whether or not to escape the output. |
||
891 | * @return string Returns a full table name. |
||
892 | 107 | */ |
|
893 | 107 | protected function prefixTable($table, bool $escape = true): string { |
|
894 | 15 | if ($table instanceof Identifier) { |
|
895 | return $escape ? $table->escape($this) : (string)$table; |
||
896 | 107 | } else { |
|
897 | 107 | $table = $this->px.$table; |
|
898 | return $escape ? $this->escape($table) : $table; |
||
899 | } |
||
900 | } |
||
901 | |||
902 | /** |
||
903 | * Strip the database prefix off a table name. |
||
904 | * |
||
905 | * @param string $table The name of the table to strip. |
||
906 | * @return string Returns the table name stripped of the prefix. |
||
907 | 12 | */ |
|
908 | 12 | protected function stripPrefix(string $table): string { |
|
909 | 12 | $len = strlen($this->px); |
|
910 | 12 | if (strcasecmp(substr($table, 0, $len), $this->px) === 0) { |
|
911 | $table = substr($table, $len); |
||
912 | 12 | } |
|
913 | return $table; |
||
914 | } |
||
915 | |||
916 | /** |
||
917 | * Optionally quote a where value. |
||
918 | * |
||
919 | * @param mixed $value The value to quote. |
||
920 | * @param string $column The column being operated on. It must already be quoted. |
||
921 | * @return string Returns the value, optionally quoted. |
||
922 | * @internal param bool $quote Whether or not to quote the value. |
||
923 | 46 | */ |
|
924 | 46 | public function quote($value, string $column = ''): string { |
|
925 | if ($value instanceof Literal) { |
||
926 | 25 | /* @var Literal $value */ |
|
927 | return $value->getValue($this, $column); |
||
928 | 41 | } else { |
|
929 | return $this->getPDO()->quote($value); |
||
930 | } |
||
931 | } |
||
932 | |||
933 | /** |
||
934 | * Gets the {@link PDO} object for this connection. |
||
935 | * |
||
936 | * @return \PDO |
||
937 | 107 | */ |
|
938 | 107 | public function getPDO(): PDO { |
|
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 ![]() |
|||
939 | return $this->pdo; |
||
940 | } |
||
941 | |||
942 | /** |
||
943 | * Set the connection to the database. |
||
944 | * |
||
945 | * @param PDO $pdo The new connection to the database. |
||
946 | * @return $this |
||
947 | */ |
||
948 | public function setPDO(PDO $pdo) { |
||
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 ![]() |
|||
949 | $this->pdo = $pdo; |
||
950 | return $this; |
||
951 | } |
||
952 | } |
||
953 |