Issues (22)

src/Db.php (4 issues)

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
(array)$options[Garden\Db\Db::OPTION_FETCH_MODE] is expanded, but the parameter $mode of PDOStatement::setFetchMode() does not expect variable arguments. ( Ignorable by Annotation )

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

768
            $stm->setFetchMode(/** @scrutinizer ignore-type */ ...(array)$options[Db::OPTION_FETCH_MODE]);
Loading history...
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 databaseConnectionSeeker.

Loading history...
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 databaseConnectionSeeker.

Loading history...
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 databaseConnectionSeeker.

Loading history...
949
        $this->pdo = $pdo;
950
        return $this;
951
    }
952
}
953