Passed
Pull Request — master (#15)
by Todd
02:41
created

Db::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 1
nop 2
dl 0
loc 6
ccs 0
cts 5
cp 0
crap 6
rs 10
c 0
b 0
f 0
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
88
        // Enum
89
        'enum' => ['type' => 'string', 'enum' => true],
90
91
        // Schema types
92
        'string' => 'varchar',
93
        'boolean' => 'bool',
94
        'integer' => 'int',
95
        'number' => 'float',
96
97
        // Other aliases
98
        'character' => 'char',
99
        'tinyint' => 'byte',
100
        'int8' => 'byte',
101
        'smallint' => 'short',
102
        'int16' => 'short',
103
        'int32' => 'int',
104
        'bigint' => 'long',
105
        'int64' => 'long',
106
        'real' => 'double'
107
    ];
108
109
    /**
110
     * @var string The database prefix.
111
     */
112
    private $px = '';
113
114
    /**
115
     * @var array A cached copy of the table schemas indexed by lowercase name.
116
     */
117
    private $tables = [];
118
119
    /**
120
     * @var array|null A cached copy of the table names indexed by lowercase name.
121
     */
122
    private $tableNames = null;
123
124
    /**
125
     * @var \PDO
126
     */
127
    private $pdo;
128
129
    /**
130
     * Initialize an instance of the {@link MySqlDb} class.
131
     *
132
     * @param PDO $pdo The connection to the database.
133
     * @param string $px The database prefix.
134
     */
135
    public function __construct(PDO $pdo, string $px = '') {
136
        $this->pdo = $pdo;
137
        $this->px = $px;
138
139
        $fetchMode = $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
140
        $this->setFetchMode(in_array($fetchMode, [0, PDO::FETCH_BOTH], true) ? PDO::FETCH_ASSOC: $fetchMode);
141
    }
142
143
    /**
144
     * Get the name of the class that handles a database driver.
145
     *
146
     * @param string|PDO $driver The name of the driver or a database connection.
147
     * @return null|string Returns the driver classname or **null** if one isn't found.
148
     */
149
    public static function driverClass($driver) {
150
        if ($driver instanceof PDO) {
151
            $name = $driver->getAttribute(PDO::ATTR_DRIVER_NAME);
152
        } else {
153
            $name = (string)$driver;
154
        }
155
156
        $name = strtolower($name);
157
        return isset(self::$drivers[$name]) ? self::$drivers[$name] : null;
158
    }
159
160
    /**
161
     * Add a table to the database.
162
     *
163
     * @param array $tableDef The table definition.
164
     * @param array $options An array of additional options when adding the table.
165
     */
166
    abstract protected function createTableDb(array $tableDef, array $options = []);
167
168
    /**
169
     * Alter a table in the database.
170
     *
171
     * When altering a table you pass an array with three optional keys: add, drop, and alter.
172
     * Each value is consists of a table definition in a format that would be passed to {@link Db::setTableDef()}.
173
     *
174
     * @param array $alterDef The alter definition.
175
     * @param array $options An array of additional options when adding the table.
176
     */
177
    abstract protected function alterTableDb(array $alterDef, array $options = []);
178
179
    /**
180
     * Drop a table.
181
     *
182
     * @param string $table The name of the table to drop.
183
     * @param array $options An array of additional options when adding the table.
184
     */
185 9
    final public function dropTable(string $table, array $options = []) {
186 9
        $options += [Db::OPTION_IGNORE => false];
187 9
        $this->dropTableDb($table, $options);
188
189 9
        $tableKey = strtolower($table);
190 9
        unset($this->tables[$tableKey], $this->tableNames[$tableKey]);
191 9
    }
192
193
    /**
194
     * Perform the actual table drop.
195
     *
196
     * @param string $table The name of the table to drop.
197
     * @param array $options An array of additional options when adding the table.
198
     */
199
    abstract protected function dropTableDb(string $table, array $options = []);
200
201
    /**
202
     * Get the names of all the tables in the database.
203
     *
204
     * @return string[] Returns an array of table names without prefixes.
205
     */
206 6
    final public function fetchTableNames() {
207 6
        if ($this->tableNames !== null) {
208 1
            return array_values($this->tableNames);
209
        }
210
211 6
        $names = $this->fetchTableNamesDb();
212
213 6
        $this->tableNames = [];
214 6
        foreach ($names as $name) {
215 6
            $name = $this->stripPrefix($name);
216 6
            $this->tableNames[strtolower($name)] = $name;
217
        }
218
219 6
        return array_values($this->tableNames);
220
    }
221
222
    /**
223
     * Fetch the table names from the underlying database layer.
224
     *
225
     * The driver should return all table names. It doesn't have to strip the prefix.
226
     *
227
     * @return string[]
228
     */
229
    abstract protected function fetchTableNamesDb();
230
231
    /**
232
     * Get a table definition.
233
     *
234
     * @param string $table The name of the table.
235
     * @return array|null Returns the table definition or null if the table does not exist.
236
     */
237 33
    final public function fetchTableDef(string $table) {
238 33
        $tableKey = strtolower($table);
239
240
        // First check the table cache.
241 33
        if (isset($this->tables[$tableKey])) {
242 25
            $tableDef = $this->tables[$tableKey];
243
244 25
            if (isset($tableDef['columns'], $tableDef['indexes'])) {
245 25
                return $tableDef;
246
            }
247 19
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
248 15
            return null;
249
        }
250
251 7
        $tableDef = $this->fetchTableDefDb($table);
252 7
        if ($tableDef !== null) {
253 6
            $this->fixIndexes($tableDef['name'], $tableDef);
254 6
            $this->tables[$tableKey] = $tableDef;
255
        }
256
257 7
        return $tableDef;
258
    }
259
260
    /**
261
     * Fetch the table definition from the database.
262
     *
263
     * @param string $table The name of the table to get.
264
     * @return array|null Returns the table def or **null** if the table doesn't exist.
265
     */
266
    abstract protected function fetchTableDefDb(string $table);
267
268
269
    /**
270
     * Get the column definitions for a table.
271
     *
272
     * @param string $table The name of the table to get the columns for.
273
     * @return array|null Returns an array of column definitions.
274
     */
275 1
    final public function fetchColumnDefs(string $table) {
276 1
        $tableKey = strtolower($table);
277
278 1
        if (!empty($this->tables[$tableKey]['columns'])) {
279
            $this->tables[$tableKey]['columns'];
280 1
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
281
            return null;
282
        }
283
284 1
        $columnDefs = $this->fetchColumnDefsDb($table);
285 1
        if ($columnDefs !== null) {
286 1
            $this->tables[$tableKey]['columns'] = $columnDefs;
287
        }
288 1
        return $columnDefs;
289
    }
290
291
    /**
292
     * Get the column definitions from the database.
293
     *
294
     * @param string $table The name of the table to fetch the columns for.
295
     * @return array|null
296
     */
297
    abstract protected function fetchColumnDefsDb(string $table);
298
299
    /**
300
     * Get the canonical type based on a type string.
301
     *
302
     * @param string $type A type string.
303
     * @return array|null Returns the type schema array or **null** if a type isn't found.
304
     */
305 34
    public static function typeDef(string $type) {
306
        // Check for the unsigned signifier.
307 34
        $unsigned = null;
308 34
        if ($type[0] === 'u') {
309 4
            $unsigned = true;
310 4
            $type = substr($type, 1);
311 32
        } elseif (preg_match('`(.+)\s+unsigned`i', $type, $m)) {
312 1
            $unsigned = true;
313 1
            $type = $m[1];
314
        }
315
316
        // Remove brackets from the type.
317 34
        $brackets = null;
318 34
        if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) {
319 15
            $brackets = $m[2];
320 15
            $type = $m[1];
321
        }
322
323
        // Look for the type.
324 34
        $type = strtolower($type);
325 34
        if (isset(self::$types[$type])) {
326 34
            $row = self::$types[$type];
327 34
            $dbtype = $type;
328
329
            // Resolve an alias.
330 34
            if (is_string($row)) {
331 2
                $dbtype = $row;
332 34
                $row = self::$types[$row];
333
            }
334
        } else {
335
            return null;
336
        }
337
338
        // Now that we have a type row we can build a schema for it.
339
        $schema = [
340 34
            'type' => $row['type'],
341 34
            'dbtype' => $dbtype
342
        ];
343
344 34
        if (!empty($row['schema'])) {
345 19
            $schema += $row['schema'];
346
        }
347
348 34
        if ($row['type'] === 'integer' && $unsigned) {
349 4
            $schema['unsigned'] = true;
350
351 4
            if (!empty($schema['maximum'])) {
352 4
                $schema['maximum'] = $schema['maximum'] * 2 + 1;
353 4
                $schema['minimum'] = 0;
354
            }
355
        }
356
357 34
        if (!empty($row['length'])) {
358 11
            $schema['maxLength'] = (int)$brackets ?: 255;
359
        }
360
361 34
        if (!empty($row['precision'])) {
362 2
            $parts = array_map('trim', explode(',', $brackets));
363 2
            $schema['precision'] = (int)$parts[0];
364 2
            if (isset($parts[1])) {
365 2
                $schema['scale'] = (int)$parts[1];
366
            }
367
        }
368
369 34
        if (!empty($row['enum'])) {
370 2
            $enum = explode(',', $brackets);
371 2
            $schema['enum'] = array_map(function ($str) {
372 2
                return trim($str, "'\" \t\n\r\0\x0B");
373 2
            }, $enum);
374
        }
375
376 34
        return $schema;
377
    }
378
379
    /**
380
     * Get the database type string from a type definition.
381
     *
382
     * This is the opposite of {@link Db::typeDef()}.
383
     *
384
     * @param array $typeDef The type definition array.
385
     * @return string Returns a db type string.
386
     */
387 19
    protected static function dbType(array $typeDef) {
388 19
        $dbtype = $typeDef['dbtype'];
389
390 19
        if (!empty($typeDef['maxLength'])) {
391 9
            $dbtype .= "({$typeDef['maxLength']})";
392 15
        } elseif (!empty($typeDef['unsigned'])) {
393
            $dbtype = 'u'.$dbtype;
394 15
        } elseif (!empty($typeDef['precision'])) {
395
            $dbtype .= "({$typeDef['precision']}";
396
            if (!empty($typeDef['scale'])) {
397
                $dbtype .= ",{$typeDef['scale']}";
398
            }
399
            $dbtype .= ')';
400 15
        } elseif (!empty($typeDef['enum'])) {
401
            $parts = array_map(function ($str) {
402
                return "'{$str}'";
403
            }, $typeDef['enum']);
404
            $dbtype .= '('.implode(',', $parts).')';
405
        }
406 19
        return $dbtype;
407
    }
408
409
410
    /**
411
     * Get the native database type based on a type schema.
412
     *
413
     * The default implementation of this method returns the canonical db types. Individual database classes will have
414
     * to override to provide any differences.
415
     *
416
     * @param array $type The type schema.
417
     * @return string
418
     */
419
    abstract protected function nativeDbType(array $type);
420
421
    /**
422
     * Set a table definition to the database.
423
     *
424
     * @param array $tableDef The table definition.
425
     * @param array $options An array of additional options when adding the table.
426
     * @throws \Exception Throws an exception if there is a mismatch in the primary key column and index.
427
     */
428 33
    final public function defineTable(array $tableDef, array $options = []) {
429 33
        $options += [Db::OPTION_DROP => false];
430
431 33
        $tableName = $tableDef['name'];
432 33
        $tableKey = strtolower($tableName);
433 33
        $tableDef['name'] = $tableName;
434 33
        $curTable = $this->fetchTableDef($tableName);
435
436 33
        $this->fixIndexes($tableName, $tableDef, $curTable);
437
438 33
        if ($this->tableNames === null) {
439
            // Fetch all tables here now so the cache knows all tables that exist.
440 6
            $this->fetchTableNames();
441
        }
442
443 33
        if (!$curTable) {
444 19
            $this->createTableDb($tableDef, $options);
445 19
            $this->tables[$tableKey] = $tableDef;
446 19
            $this->tableNames[$tableKey] = $tableDef['name'];
447 19
            return;
448
        }
449
        // This is the alter statement.
450 23
        $alterDef = ['name' => $tableName];
451
452
        // Figure out the columns that have changed.
453 23
        $curColumns = (array)$curTable['columns'];
454 23
        $newColumns = (array)$tableDef['columns'];
455
456 23
        $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns);
457 23
        $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) {
458 23
            $search = ['dbtype', 'allowNull', 'default', 'maxLength'];
459 23
            foreach ($search as $key) {
460 23
                if (self::val($key, $curr) !== self::val($key, $new)) {
461
                    // Return 0 if the values are different, not the same.
462 4
                    return 0;
463
                }
464
            }
465
466
            // Enum checking.
467 22
            if (isset($curr['enum']) xor isset($new['enum'])) {
468
                return 0;
469 22
            } elseif (isset($curr['enum']) && isset($new['enum'])
470
                && (
471 1
                    count($curr['enum']) !== count($new['enum'])
472 22
                    || !empty(array_diff($curr['enum'], $new['enum']))
473
                )
474
            ) {
475 1
                return 0;
476
            }
477
478 21
            return 1;
479 23
        });
480
481
        // Figure out the indexes that have changed.
482 23
        $curIndexes = (array)self::val('indexes', $curTable, []);
483 23
        $newIndexes = (array)self::val('indexes', $tableDef, []);
484
485 23
        $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']);
486
487 23
        $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']);
488 23
        if ($options[Db::OPTION_DROP]) {
489 1
            $alterDef['drop']['columns'] = array_diff_key($curColumns, $newColumns);
490 1
            $alterDef['drop']['indexes'] = $dropIndexes;
491
        } else {
492 22
            $alterDef['drop']['columns'] = [];
493 22
            $alterDef['drop']['indexes'] = [];
494
495
            // If the primary key has changed then the old one needs to be dropped.
496 22
            if ($pk = $this->findPrimaryKeyIndex($dropIndexes)) {
497 2
                $alterDef['drop']['indexes'][] = $pk;
498
            }
499
        }
500
501
        // Check to see if any alterations at all need to be made.
502 23
        if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) &&
503 23
            empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) &&
504 23
            empty($alterDef['alter']['columns'])
505
        ) {
506 15
            return;
507
        }
508
509 8
        $alterDef['def'] = $tableDef;
510
511
        // Alter the table.
512 8
        $this->alterTableDb($alterDef, $options);
513
514
        // Update the cached schema.
515 8
        $tableDef['name'] = $tableName;
516 8
        $this->tables[$tableKey] = $tableDef;
517
518 8
        $this->tableNames[$tableKey] = $tableName;
519 8
    }
520
521
    /**
522
     * Find the primary key in an array of indexes.
523
     *
524
     * @param array $indexes The indexes to search.
525
     * @return array|null Returns the primary key or **null** if there isn't one.
526
     */
527 33
    protected function findPrimaryKeyIndex(array $indexes) {
528 33
        foreach ($indexes as $index) {
529 13
            if ($index['type'] === Db::INDEX_PK) {
530 10
                return $index;
531
            }
532
        }
533 24
        return null;
534
    }
535
536
    /**
537
     * Move the primary key index into the correct place for database drivers.
538
     *
539
     * @param string $tableName The name of the table.
540
     * @param array &$tableDef The table definition.
541
     * @param array|null $curTableDef The current database table def used to resolve conflicts in some names.
542
     * @throws \Exception Throws an exception when there is a mismatch between the primary index and the primary key
543
     * defined on the columns themselves.
544
     */
545 33
    private function fixIndexes(string $tableName, array &$tableDef, $curTableDef = null) {
546 33
        $tableDef += ['indexes' => []];
547
548
        // Loop through the columns and add the primary key index.
549 33
        $primaryColumns = [];
550 33
        foreach ($tableDef['columns'] as $cname => $cdef) {
551 33
            if (!empty($cdef['primary'])) {
552 7
                $primaryColumns[] = $cname;
553
            }
554
        }
555
556
        // Massage the primary key index.
557 33
        $primaryFound = false;
558 33
        foreach ($tableDef['indexes'] as &$indexDef) {
559 27
            $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null];
560
561 27
            if ($indexDef['type'] === Db::INDEX_PK) {
562 9
                $primaryFound = true;
563
564 9
                if (empty($primaryColumns)) {
565 5
                    foreach ($indexDef['columns'] as $cname) {
566 5
                        $tableDef['columns'][$cname]['primary'] = true;
567
                    }
568 4
                } elseif (array_diff($primaryColumns, $indexDef['columns'])) {
569 9
                    throw new \Exception("There is a mismatch in the primary key index and primary key columns.", 500);
570
                }
571 23
            } elseif (isset($curTableDef['indexes'])) {
572 16
                foreach ($curTableDef['indexes'] as $curIndexDef) {
573 16
                    if ($this->indexCompare($indexDef, $curIndexDef) === 0) {
574 16
                        if (!empty($curIndexDef['name'])) {
575 16
                            $indexDef['name'] = $curIndexDef['name'];
576
                        }
577 16
                        break;
578
                    }
579
                }
580
            }
581
        }
582
583 33
        if (!$primaryFound && !empty($primaryColumns)) {
584 3
            $tableDef['indexes'][] = [
585 3
                'columns' => $primaryColumns,
586
                'type' => Db::INDEX_PK
587
            ];
588
        }
589 33
    }
590
591
    /**
592
     * Get the database prefix.
593
     *
594
     * @return string Returns the current db prefix.
595
     */
596 6
    public function getPx(): string {
597 6
        return $this->px;
598
    }
599
600
    /**
601
     * Set the database prefix.
602
     *
603
     * @param string $px The new database prefix.
604
     */
605
    public function setPx(string $px) {
606
        $this->px = $px;
607
    }
608
609
    /**
610
     * Compare two index definitions to see if they have the same columns and same type.
611
     *
612
     * @param array $a The first index.
613
     * @param array $b The second index.
614
     * @return int Returns an integer less than, equal to, or greater than zero if {@link $a} is
615
     * considered to be respectively less than, equal to, or greater than {@link $b}.
616
     */
617 19
    private function indexCompare(array $a, array $b): int {
618 19
        if ($a['columns'] > $b['columns']) {
619 3
            return 1;
620 19
        } elseif ($a['columns'] < $b['columns']) {
621 3
            return -1;
622
        }
623
624 17
        return strcmp(
625 17
            isset($a['type']) ? $a['type'] : '',
626 17
            isset($b['type']) ? $b['type'] : ''
627
        );
628
    }
629
630
    /**
631
     * Get data from the database.
632
     *
633
     * @param string|Identifier $table The name of the table to get the data from.
634
     * @param array $where An array of where conditions.
635
     * @param array $options An array of additional options.
636
     * @return \PDOStatement Returns the result set.
637
     */
638
    abstract public function get($table, array $where, array $options = []): \PDOStatement;
639
640
    /**
641
     * Get a single row from the database.
642
     *
643
     * This is a convenience method that calls {@link Db::get()} and shifts off the first row.
644
     *
645
     * @param string|Identifier $table The name of the table to get the data from.
646
     * @param array $where An array of where conditions.
647
     * @param array $options An array of additional options.
648
     * @return array|object|null Returns the row or false if there is no row.
649
     */
650 8
    final public function getOne($table, array $where, array $options = []) {
651 8
        $rows = $this->get($table, $where, $options);
652 8
        $row = $rows->fetch();
653
654 8
        return $row === false ? null : $row;
655
    }
656
657
    /**
658
     * Insert a row into a table.
659
     *
660
     * @param string $table The name of the table to insert into.
661
     * @param array $row The row of data to insert.
662
     * @param array $options An array of options for the insert.
663
     *
664
     * Db::OPTION_IGNORE
665
     * : Whether or not to ignore inserts that lead to a duplicate key. *default false*
666
     * Db::OPTION_REPLACE
667
     * : Whether or not to replace duplicate keys. *default false*
668
     * Db::OPTION_UPSERT
669
     * : Whether or not to update the existing data when duplicate keys exist.
670
     *
671
     * @return mixed Returns the id of the inserted record, **true** if the table doesn't have an auto increment, or **false** otherwise.
672
     * @see Db::load()
673
     */
674
    abstract public function insert(string $table, array $row, array $options = []);
675
676
    /**
677
     * Load many rows into a table.
678
     *
679
     * @param string $table The name of the table to insert into.
680
     * @param \Traversable|array $rows A dataset to insert.
681
     * Note that all rows must contain the same columns.
682
     * The first row will be looked at for the structure of the insert and the rest of the rows will use this structure.
683
     * @param array $options An array of options for the inserts. See {@link Db::insert()} for details.
684
     * @see Db::insert()
685
     */
686
    public function load(string $table, $rows, array $options = []) {
687
        foreach ($rows as $row) {
688
            $this->insert($table, $row, $options);
689
        }
690
    }
691
692
693
    /**
694
     * Update a row or rows in a table.
695
     *
696
     * @param string $table The name of the table to update.
697
     * @param array $set The values to set.
698
     * @param array $where The where filter for the update.
699
     * @param array $options An array of options for the update.
700
     * @return int Returns the number of affected rows.
701
     */
702
    abstract public function update(string $table, array $set, array $where, array $options = []): int;
703
704
    /**
705
     * Delete rows from a table.
706
     *
707
     * @param string $table The name of the table to delete from.
708
     * @param array $where The where filter of the delete.
709
     * @param array $options An array of options.
710
     *
711
     * Db:OPTION_TRUNCATE
712
     * : Truncate the table instead of deleting rows. In this case {@link $where} must be blank.
713
     * @return int Returns the number of affected rows.
714
     */
715
    abstract public function delete(string $table, array $where, array $options = []): int;
716
717
    /**
718
     * Reset the internal table definition cache.
719
     *
720
     * @return $this
721
     */
722 7
    public function reset() {
723 7
        $this->tables = [];
724 7
        $this->tableNames = null;
725 7
        return $this;
726
    }
727
728
    /**
729
     * Build a standardized index name from an index definition.
730
     *
731
     * @param string $tableName The name of the table the index is in.
732
     * @param array $indexDef The index definition.
733
     * @return string Returns the index name.
734
     */
735 27
    protected function buildIndexName(string $tableName, array $indexDef): string {
736 27
        $indexDef += ['type' => Db::INDEX_IX, 'suffix' => ''];
737
738 27
        $type = $indexDef['type'];
739
740 27
        if ($type === Db::INDEX_PK) {
741 9
            return 'primary';
742
        }
743 23
        $px = self::val($type, [Db::INDEX_IX => 'ix_', Db::INDEX_UNIQUE => 'ux_'], 'ix_');
744 23
        $sx = $indexDef['suffix'];
745 23
        $result = $px.$tableName.'_'.($sx ?: implode('', $indexDef['columns']));
746 23
        return $result;
747
    }
748
749
    /**
750
     * Execute a query that fetches data.
751
     *
752
     * @param string $sql The query to execute.
753
     * @param array $params Input parameters for the query.
754
     * @param array $options Additional options.
755
     * @return \PDOStatement Returns the result of the query.
756
     * @throws \PDOException Throws an exception if something went wrong during the query.
757
     */
758 43
    protected function query(string $sql, array $params = [], array $options = []): \PDOStatement {
759
        $options += [
760 43
            Db::OPTION_FETCH_MODE => $this->getFetchArgs()
761
        ];
762
763 43
        $stm = $this->getPDO()->prepare($sql);
764
765
766 43
        if ($options[Db::OPTION_FETCH_MODE]) {
767 36
            $stm->setFetchMode(...(array)$options[Db::OPTION_FETCH_MODE]);
0 ignored issues
show
Bug introduced by
(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

767
            $stm->setFetchMode(/** @scrutinizer ignore-type */ ...(array)$options[Db::OPTION_FETCH_MODE]);
Loading history...
768
        }
769
770 43
        $r = $stm->execute($params);
771
772
        // This is a kludge for those that don't have errors turning into exceptions.
773 43
        if ($r === false) {
774
            list($state, $code, $msg) = $stm->errorInfo();
775
            throw new \PDOException($msg, $code);
776
        }
777
778 43
        return $stm;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stm could return the type boolean which is incompatible with the type-hinted return PDOStatement. Consider adding an additional type-check to rule them out.
Loading history...
779
    }
780
781
    /**
782
     * Query the database and return a row count.
783
     *
784
     * @param string $sql The query to execute.
785
     * @param array $params Input parameters for the query.
786
     * @param array $options Additional options.
787
     * @return int
788
     */
789 21
    protected function queryModify(string $sql, array $params = [], array $options = []): int {
790 21
        $options += [Db::OPTION_FETCH_MODE => 0];
791 21
        $stm = $this->query($sql, $params, $options);
792 21
        return $stm->rowCount();
793
    }
794
795
    /**
796
     * Query the database and return the ID of the record that was inserted.
797
     *
798
     * @param string $sql The query to execute.
799
     * @param array $params Input parameters for the query.
800
     * @param array $options Additional options.
801
     * @return mixed Returns the record ID.
802
     */
803 11
    protected function queryID(string $sql, array $params = [], array $options = []) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
804 11
        $options += [Db::OPTION_FETCH_MODE => 0];
805 11
        $this->query($sql, $params, $options);
806 11
        $r = $this->getPDO()->lastInsertId();
807
808 11
        return is_numeric($r) ? (int)$r : $r;
809
    }
810
811
    /**
812
     * Query the database for a database define.
813
     *
814
     * @param string $sql The query to execute.
815
     * @param array $options Additional options.
816
     */
817 19
    protected function queryDefine(string $sql, array $options = []) {
818 19
        $options += [Db::OPTION_FETCH_MODE => 0];
819 19
        $this->query($sql, [], $options);
820 19
    }
821
822
    /**
823
     * Safely get a value out of an array.
824
     *
825
     * This function will always return a value even if the array key doesn't exist.
826
     * The self::val() function is one of the biggest workhorses of Vanilla and shows up a lot throughout other code.
827
     * It's much preferable to use this function if your not sure whether or not an array key exists rather than
828
     * using @ error suppression.
829
     *
830
     * This function uses optimizations found in the [facebook libphputil library](https://github.com/facebook/libphutil).
831
     *
832
     * @param string|int $key The array key.
833
     * @param array|object $array The array to get the value from.
834
     * @param mixed $default The default value to return if the key doesn't exist.
835
     * @return mixed The item from the array or `$default` if the array key doesn't exist.
836
     * @category Array Functions
837
     */
838 42
    protected static function val($key, $array, $default = null) {
839 42
        if (is_array($array)) {
840
            // isset() is a micro-optimization - it is fast but fails for null values.
841 42
            if (isset($array[$key])) {
842 38
                return $array[$key];
843
            }
844
845
            // Comparing $default is also a micro-optimization.
846 41
            if ($default === null || array_key_exists($key, $array)) {
847 41
                return null;
848
            }
849
        } elseif (is_object($array)) {
850
            if (isset($array->$key)) {
851
                return $array->$key;
852
            }
853
854
            if ($default === null || property_exists($array, $key)) {
855
                return null;
856
            }
857
        }
858
859 2
        return $default;
860
    }
861
862
    /**
863
     * Escape an identifier.
864
     *
865
     * @param string|Literal $identifier The identifier to escape.
866
     * @return string Returns the field properly escaped.
867
     */
868 45
    public function escape($identifier): string {
869 45
        if ($identifier instanceof Literal) {
870 1
            return $identifier->getValue($this);
871
        }
872 45
        return '`'.str_replace('`', '``', $identifier).'`';
873
    }
874
875
    /**
876
     * Escape a a like string so that none of its characters work as wildcards.
877
     *
878
     * @param string $str The string to escape.
879
     * @return string Returns an escaped string.
880
     */
881 6
    protected function escapeLike(string $str): string {
882 6
        return addcslashes($str, '_%');
883
    }
884
885
    /**
886
     * Prefix a table name.
887
     *
888
     * @param string|Identifier $table The name of the table to prefix.
889
     * @param bool $escape Whether or not to escape the output.
890
     * @return string Returns a full table name.
891
     */
892 45
    protected function prefixTable($table, bool $escape = true): string {
893 45
        if ($table instanceof Identifier) {
894 6
            return $escape ? $table->escape($this) : (string)$table;
895
        } else {
896 45
            $table = $this->px.$table;
897 45
            return $escape ? $this->escape($table) : $table;
898
        }
899
    }
900
901
    /**
902
     * Strip the database prefix off a table name.
903
     *
904
     * @param string $table The name of the table to strip.
905
     * @return string Returns the table name stripped of the prefix.
906
     */
907 6
    protected function stripPrefix(string $table): string {
908 6
        $len = strlen($this->px);
909 6
        if (strcasecmp(substr($table, 0, $len), $this->px) === 0) {
910 6
            $table = substr($table, $len);
911
        }
912 6
        return $table;
913
    }
914
915
    /**
916
     * Optionally quote a where value.
917
     *
918
     * @param mixed $value The value to quote.
919
     * @param string $column The column being operated on. It must already be quoted.
920
     * @return string Returns the value, optionally quoted.
921
     * @internal param bool $quote Whether or not to quote the value.
922
     */
923
    public function quote($value, string $column = ''): string {
924
        if ($value instanceof Literal) {
925
            /* @var Literal $value */
926
            return $value->getValue($this, $column);
927
        } else {
928
            return $this->getPDO()->quote($value);
929
        }
930
    }
931
932
    /**
933
     * Gets the {@link PDO} object for this connection.
934
     *
935
     * @return \PDO
936
     */
937 45
    public function getPDO(): PDO {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
938 45
        return $this->pdo;
939
    }
940
941
    /**
942
     * Set the connection to the database.
943
     *
944
     * @param PDO $pdo The new connection to the database.
945
     * @return $this
946
     */
947
    public function setPDO(PDO $pdo) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

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