Issues (22)

src/Db.php (1 issue)

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 = []) {
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 {
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) {
949
        $this->pdo = $pdo;
950
        return $this;
951
    }
952
}
953