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

Table::alterTable()   F

Complexity

Conditions 18
Paths 324

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 27
nc 324
nop 9
dl 0
loc 41
rs 2.4833
c 1
b 0
f 0

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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