Passed
Push — main ( 989106...9612b3 )
by Thierry
02:02
created

Table::tablesStatuses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Driver\Sqlite\Db;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
7
use Lagdo\DbAdmin\Driver\Entity\IndexEntity;
8
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity;
9
10
use Lagdo\DbAdmin\Driver\Db\ConnectionInterface;
11
12
use Lagdo\DbAdmin\Driver\Db\Table as AbstractTable;
13
14
class Table extends AbstractTable
15
{
16
    /**
17
     * @inheritDoc
18
     */
19
    public function tableHelp(string $name)
20
    {
21
        if ($name == "sqlite_sequence") {
22
            return "fileformat2.html#seqtab";
23
        }
24
        if ($name == "sqlite_master") {
25
            return "fileformat2.html#$name";
26
        }
27
    }
28
29
    /**
30
     * @param string $table
31
     *
32
     * @return array
33
     */
34
    private function queryStatus(string $table = '')
35
    {
36
        $query = "SELECT name AS Name, type AS Engine, 'rowid' AS Oid, '' AS Auto_increment " .
37
            "FROM sqlite_master WHERE type IN ('table', 'view') " .
38
            ($table != "" ? "AND name = " . $this->driver->quote($table) : "ORDER BY name");
39
        return $this->driver->rows($query);
40
    }
41
42
    /**
43
     * @param array $row
44
     *
45
     * @return TableEntity
46
     */
47
    private function makeStatus(array $row)
48
    {
49
        $status = new TableEntity($row['Name']);
50
        $status->engine = $row['Engine'];
51
        $status->oid = $row['Oid'];
52
        // $status->Auto_increment = $row['Auto_increment'];
53
        $status->rows = $this->connection->result("SELECT COUNT(*) FROM " . $this->driver->escapeId($row["Name"]));
54
        $tables[$row["Name"]] = $status;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$tables was never initialized. Although not strictly required by PHP, it is generally a good practice to add $tables = array(); before regardless.
Loading history...
55
56
        return $status;
57
    }
58
59
    /**
60
     * @inheritDoc
61
     */
62
    public function tableStatus(string $table, bool $fast = false)
63
    {
64
        $rows = $this->queryStatus($table);
65
        if (!($row = reset($rows))) {
66
            return null;
67
        }
68
        return $this->makeStatus($row);
69
    }
70
71
    /**
72
     * @inheritDoc
73
     */
74
    public function tablesStatuses(bool $fast = false)
75
    {
76
        $tables = [];
77
        $rows = $this->queryStatus();
78
        foreach ($rows as $row) {
79
            $tables[$row["Name"]] = $this->makeStatus($row);
80
        }
81
        return $tables;
82
    }
83
84
    /**
85
     * @inheritDoc
86
     */
87
    public function isView(TableEntity $tableStatus)
88
    {
89
        return $tableStatus->engine == "view";
90
    }
91
92
    /**
93
     * @inheritDoc
94
     */
95
    public function supportForeignKeys(TableEntity $tableStatus)
96
    {
97
        return !$this->connection->result("SELECT sqlite_compileoption_used('OMIT_FOREIGN_KEY')");
98
    }
99
100
    /**
101
     * @inheritDoc
102
     */
103
    public function fields(string $table)
104
    {
105
        $fields = [];
106
        $primary = "";
107
        foreach ($this->driver->rows("PRAGMA table_info(" . $this->driver->table($table) . ")") as $row) {
108
            $name = $row["name"];
109
            $type = strtolower($row["type"]);
110
            $default = $row["dflt_value"];
111
112
            $field = new TableFieldEntity();
113
114
            $field->name = $name;
115
            $field->type = (preg_match('~int~i', $type) ? "integer" : (preg_match('~char|clob|text~i', $type) ?
116
                "text" : (preg_match('~blob~i', $type) ? "blob" : (preg_match('~real|floa|doub~i', $type) ?
117
                "real" : "numeric"))));
118
            $field->fullType = $type;
119
            $field->default = (preg_match("~'(.*)'~", $default, $match) ? str_replace("''", "'", $match[1]) :
120
                ($default == "NULL" ? null : $default));
121
            $field->null = !$row["notnull"];
122
            $field->privileges = ["select" => 1, "insert" => 1, "update" => 1];
123
            $field->primary = $row["pk"];
124
125
            if ($row["pk"]) {
126
                if ($primary != "") {
127
                    $fields[$primary]->autoIncrement = false;
128
                } elseif (preg_match('~^integer$~i', $type)) {
129
                    $field->autoIncrement = true;
130
                }
131
                $primary = $name;
132
            }
133
134
            $fields[$field->name] = $field;
135
        }
136
        $query = "SELECT sql FROM sqlite_master WHERE type IN ('table', 'view') AND name = " . $this->driver->quote($table);
137
        $result = $this->connection->result($query);
138
        preg_match_all('~(("[^"]*+")+|[a-z0-9_]+)\s+text\s+COLLATE\s+(\'[^\']+\'|\S+)~i',
139
            $result, $matches, PREG_SET_ORDER);
140
        foreach ($matches as $match) {
141
            $name = str_replace('""', '"', preg_replace('~^"|"$~', '', $match[1]));
142
            if (isset($fields[$name])) {
143
                $fields[$name]->collation = trim($match[3], "'");
144
            }
145
        }
146
        return $fields;
147
    }
148
149
    /**
150
     * @inheritDoc
151
     */
152
    public function indexes(string $table, ConnectionInterface $connection = null)
153
    {
154
        if (!is_object($connection)) {
155
            $connection = $this->connection;
156
        }
157
        $indexes = [];
158
        $query = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = " . $this->driver->quote($table);
159
        $result = $connection->result($query);
160
        if (preg_match('~\bPRIMARY\s+KEY\s*\((([^)"]+|"[^"]*"|`[^`]*`)++)~i', $result, $match)) {
161
            $indexes[""] = new IndexEntity();
162
            $indexes[""]->type = "PRIMARY";
163
            preg_match_all('~((("[^"]*+")+|(?:`[^`]*+`)+)|(\S+))(\s+(ASC|DESC))?(,\s*|$)~i',
164
                $match[1], $matches, PREG_SET_ORDER);
165
            foreach ($matches as $match) {
166
                $indexes[""]->columns[] = $this->driver->unescapeId($match[2]) . $match[4];
167
                $indexes[""]->descs[] = (preg_match('~DESC~i', $match[5]) ? '1' : null);
168
            }
169
        }
170
        if (!$indexes) {
171
            foreach ($this->fields($table) as $name => $field) {
172
                if ($field->primary) {
173
                    if (!isset($indexes[""])) {
174
                        $indexes[""] = new IndexEntity();
175
                    }
176
                    $indexes[""]->type = "PRIMARY";
177
                    $indexes[""]->columns = [$name];
178
                    $indexes[""]->lengths = [];
179
                    $indexes[""]->descs = [null];
180
                }
181
            }
182
        }
183
        $query = "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = " . $this->driver->quote($table);
184
        $results = $this->driver->keyValues($query, $connection);
185
        foreach ($this->driver->rows("PRAGMA index_list(" . $this->driver->table($table) . ")", $connection) as $row) {
186
            $index = new IndexEntity();
187
188
            $name = $row["name"];
189
            $index->type = $row["unique"] ? "UNIQUE" : "INDEX";
190
            $index->lengths = [];
191
            $index->descs = [];
192
            foreach ($this->driver->rows("PRAGMA index_info(" . $this->driver->escapeId($name) . ")", $connection) as $row1) {
193
                $index->columns[] = $row1["name"];
194
                $index->descs[] = null;
195
            }
196
            if (preg_match('~^CREATE( UNIQUE)? INDEX ' . preg_quote($this->driver->escapeId($name) . ' ON ' .
197
                $this->driver->escapeId($table), '~') . ' \((.*)\)$~i', $results[$name], $regs)) {
198
                preg_match_all('/("[^"]*+")+( DESC)?/', $regs[2], $matches);
199
                foreach ($matches[2] as $key => $val) {
200
                    if ($val) {
201
                        $index->descs[$key] = '1';
202
                    }
203
                }
204
            }
205
            if (!$indexes[""] || $index->type != "UNIQUE" || $index->columns != $indexes[""]->columns ||
206
                $index->descs != $indexes[""]->descs || !preg_match("~^sqlite_~", $name)) {
207
                $indexes[$name] = $index;
208
            }
209
        }
210
        return $indexes;
211
    }
212
213
    /**
214
     * @inheritDoc
215
     */
216
    public function foreignKeys(string $table)
217
    {
218
        $foreignKeys = [];
219
        foreach ($this->driver->rows("PRAGMA foreign_key_list(" . $this->driver->table($table) . ")") as $row) {
220
            $name = $row["id"];
221
            if (!isset($foreignKeys[$name])) {
222
                $foreignKeys[$name] = new ForeignKeyEntity();
223
            }
224
            //! idf_unescape in SQLite2
225
            $foreignKeys[$name]->source[] = $row["from"];
226
            $foreignKeys[$name]->target[] = $row["to"];
227
        }
228
        return $foreignKeys;
229
    }
230
231
    /**
232
     * @inheritDoc
233
     */
234
    public function alterTable(string $table, string $name, array $fields, array $foreign,
235
        string $comment, string $engine, string $collation, int $autoIncrement, string $partitioning)
236
    {
237
        $use_all_fields = ($table == "" || !empty($foreign));
238
        foreach ($fields as $field) {
239
            if ($field[0] != "" || !$field[1] || $field[2]) {
240
                $use_all_fields = true;
241
                break;
242
            }
243
        }
244
        $alter = [];
245
        $originals = [];
246
        foreach ($fields as $field) {
247
            if ($field[1]) {
248
                $alter[] = ($use_all_fields ? $field[1] : "ADD " . implode($field[1]));
249
                if ($field[0] != "") {
250
                    $originals[$field[0]] = $field[1][0];
251
                }
252
            }
253
        }
254
        if (!$use_all_fields) {
255
            foreach ($alter as $val) {
256
                if (!$this->driver->execute("ALTER TABLE " . $this->driver->table($table) . " $val")) {
257
                    return false;
258
                }
259
            }
260
            if ($table != $name && !$this->driver->execute("ALTER TABLE " . $this->driver->table($table) . " RENAME TO " . $this->driver->table($name))) {
261
                return false;
262
            }
263
        } elseif (!$this->recreateTable($table, $name, $alter, $originals, $foreign, $autoIncrement)) {
264
            return false;
265
        }
266
        if ($autoIncrement) {
267
            $this->driver->execute("BEGIN");
268
            $this->driver->execute("UPDATE sqlite_sequence SET seq = $autoIncrement WHERE name = " . $this->driver->quote($name)); // ignores error
269
            if (!$this->driver->affectedRows()) {
270
                $this->driver->execute("INSERT INTO sqlite_sequence (name, seq) VALUES (" . $this->driver->quote($name) . ", $autoIncrement)");
271
            }
272
            $this->driver->execute("COMMIT");
273
        }
274
        return true;
275
    }
276
277
    /**
278
     * Recreate a table
279
     *
280
     * @param string $table
281
     * @param string $name
282
     * @param array $fields
283
     * @param array $originals
284
     * @param array $foreign
285
     * @param integer $autoIncrement
286
     * @param array $indexes
287
     *
288
     * @return bool
289
     */
290
    protected function recreateTable(string $table, string $name, array $fields, array $originals,
291
        array $foreign, int $autoIncrement, array $indexes = [])
292
    {
293
        if ($table != "") {
294
            if (empty($fields)) {
295
                foreach ($this->fields($table) as $key => $field) {
296
                    if (!empty($indexes)) {
297
                        $field->autoIncrement = 0;
298
                    }
299
                    $fields[] = $this->util->processField($field, $field);
300
                    $originals[$key] = $this->driver->escapeId($key);
301
                }
302
            }
303
            $primary_key = false;
304
            foreach ($fields as $field) {
305
                if ($field[6]) {
306
                    $primary_key = true;
307
                }
308
            }
309
            $drop_indexes = [];
310
            foreach ($indexes as $key => $val) {
311
                if ($val[2] == "DROP") {
312
                    $drop_indexes[$val[1]] = true;
313
                    unset($indexes[$key]);
314
                }
315
            }
316
            foreach ($this->indexes($table) as $key_name => $index) {
317
                $columns = [];
318
                foreach ($index->columns as $key => $column) {
319
                    if (!$originals[$column]) {
320
                        continue 2;
321
                    }
322
                    $columns[] = $originals[$column] . ($index->descs[$key] ? " DESC" : "");
323
                }
324
                if (!$drop_indexes[$key_name]) {
325
                    if ($index->type != "PRIMARY" || !$primary_key) {
326
                        $indexes[] = [$index->type, $key_name, $columns];
327
                    }
328
                }
329
            }
330
            foreach ($indexes as $key => $val) {
331
                if ($val[0] == "PRIMARY") {
332
                    unset($indexes[$key]);
333
                    $foreign[] = "  PRIMARY KEY (" . implode(", ", $val[2]) . ")";
334
                }
335
            }
336
            foreach ($this->foreignKeys($table) as $key_name => $foreignKey) {
337
                foreach ($foreignKey->source as $key => $column) {
338
                    if (!$originals[$column]) {
339
                        continue 2;
340
                    }
341
                    $foreignKey->source[$key] = $this->driver->unescapeId($originals[$column]);
342
                }
343
                if (!isset($foreign[" $key_name"])) {
344
                    $foreign[] = " " . $this->driver->formatForeignKey($foreignKey);
345
                }
346
            }
347
            $this->driver->execute("BEGIN");
348
        }
349
        foreach ($fields as $key => $field) {
350
            $fields[$key] = "  " . implode($field);
351
        }
352
        $fields = array_merge($fields, array_filter($foreign));
353
        $tempName = ($table == $name ? "adminer_$name" : $name);
354
        if (!$this->driver->execute("CREATE TABLE " . $this->driver->table($tempName) .
355
            " (\n" . implode(",\n", $fields) . "\n)")) {
356
            // implicit ROLLBACK to not overwrite $this->driver->error()
357
            return false;
358
        }
359
        if ($table != "") {
360
            if ($originals && !$this->driver->execute("INSERT INTO " . $this->driver->table($tempName) .
361
                " (" . implode(", ", $originals) . ") SELECT " . implode(
362
                    ", ",
363
                    array_map(function ($key) {
364
                   return $this->driver->escapeId($key);
365
               }, array_keys($originals))
366
                ) . " FROM " . $this->driver->table($table))) {
367
                return false;
368
            }
369
            $triggers = [];
370
            foreach ($this->triggers($table) as $trigger_name => $timing_event) {
371
                $trigger = $this->trigger($trigger_name);
372
                $triggers[] = "CREATE TRIGGER " . $this->driver->escapeId($trigger_name) . " " .
373
                    implode(" ", $timing_event) . " ON " . $this->driver->table($name) . "\n$trigger[Statement]";
374
            }
375
            $autoIncrement = $autoIncrement ? 0 :
376
                $this->connection->result("SELECT seq FROM sqlite_sequence WHERE name = " .
377
                $this->driver->quote($table)); // if $autoIncrement is set then it will be updated later
378
            // drop before creating indexes and triggers to allow using old names
379
            if (!$this->driver->execute("DROP TABLE " . $this->driver->table($table)) ||
380
                ($table == $name && !$this->driver->execute("ALTER TABLE " . $this->driver->table($tempName) .
381
                " RENAME TO " . $this->driver->table($name))) || !$this->alterIndexes($name, $indexes)
382
            ) {
383
                return false;
384
            }
385
            if ($autoIncrement) {
386
                $this->driver->execute("UPDATE sqlite_sequence SET seq = $autoIncrement WHERE name = " . $this->driver->quote($name)); // ignores error
387
            }
388
            foreach ($triggers as $trigger) {
389
                if (!$this->driver->execute($trigger)) {
390
                    return false;
391
                }
392
            }
393
            $this->driver->execute("COMMIT");
394
        }
395
        return true;
396
    }
397
398
    /**
399
     * @inheritDoc
400
     */
401
    public function alterIndexes(string $table, array $alter)
402
    {
403
        foreach ($alter as $primary) {
404
            if ($primary[0] == "PRIMARY") {
405
                return $this->recreateTable($table, $table, [], [], [], 0, $alter);
406
            }
407
        }
408
        foreach (array_reverse($alter) as $val) {
409
            if (!$this->driver->execute($val[2] == "DROP" ?
410
                "DROP INDEX " . $this->driver->escapeId($val[1]) :
411
                $this->driver->sqlForCreateIndex($table, $val[0], $val[1], "(" . implode(", ", $val[2]) . ")")
412
            )) {
413
                return false;
414
            }
415
        }
416
        return true;
417
    }
418
419
    /**
420
     * @inheritDoc
421
     */
422
    public function trigger(string $trigger)
423
    {
424
        if ($trigger == "") {
425
            return ["Statement" => "BEGIN\n\t;\nEND"];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('Statement' => 'BEGIN ; END') returns the type array<string,string> which is incompatible with the return type mandated by Lagdo\DbAdmin\Driver\Db\TableInterface::trigger() of Lagdo\DbAdmin\Driver\Entity\TriggerEntity.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
426
        }
427
        $idf = '(?:[^`"\s]+|`[^`]*`|"[^"]*")+';
428
        $options = $this->triggerOptions();
429
        preg_match("~^CREATE\\s+TRIGGER\\s*$idf\\s*(" . implode("|", $options["Timing"]) .
430
            ")\\s+([a-z]+)(?:\\s+OF\\s+($idf))?\\s+ON\\s*$idf\\s*(?:FOR\\s+EACH\\s+ROW\\s)?(.*)~is",
431
            $this->connection->result("SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = " .
432
            $this->driver->quote($trigger)), $match);
433
        $of = $match[3];
434
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('Timing' =>...tatement' => $match[4]) returns the type array<string,mixed|string> which is incompatible with the return type mandated by Lagdo\DbAdmin\Driver\Db\TableInterface::trigger() of Lagdo\DbAdmin\Driver\Entity\TriggerEntity.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
435
            "Timing" => strtoupper($match[1]),
436
            "Event" => strtoupper($match[2]) . ($of ? " OF" : ""),
437
            "Of" => ($of[0] == '`' || $of[0] == '"' ? $this->driver->unescapeId($of) : $of),
438
            "Trigger" => $trigger,
439
            "Statement" => $match[4],
440
        ];
441
    }
442
443
    /**
444
     * @inheritDoc
445
     */
446
    public function triggers(string $table)
447
    {
448
        $triggers = [];
449
        $options = $this->triggerOptions();
450
        $query = "SELECT * FROM sqlite_master WHERE type = 'trigger' AND tbl_name = " . $this->driver->quote($table);
451
        foreach ($this->driver->rows($query) as $row) {
452
            preg_match('~^CREATE\s+TRIGGER\s*(?:[^`"\s]+|`[^`]*`|"[^"]*")+\s*(' .
453
                implode("|", $options["Timing"]) . ')\s*(.*?)\s+ON\b~i', $row["sql"], $match);
454
            $triggers[$row["name"]] = new Trigger($match[1], $match[2]);
0 ignored issues
show
Bug introduced by
The type Lagdo\DbAdmin\Driver\Sqlite\Db\Trigger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
455
        }
456
        return $triggers;
457
    }
458
459
    /**
460
     * @inheritDoc
461
     */
462
    public function triggerOptions()
463
    {
464
        return [
465
            "Timing" => ["BEFORE", "AFTER", "INSTEAD OF"],
466
            "Event" => ["INSERT", "UPDATE", "UPDATE OF", "DELETE"],
467
            "Type" => ["FOR EACH ROW"],
468
        ];
469
    }
470
}
471