Passed
Push — main ( c996bf...50ff31 )
by Thierry
01:51
created

Table::alterTable()   F

Complexity

Conditions 26
Paths 8784

Size

Total Lines 57
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 39
c 0
b 0
f 0
nc 8784
nop 9
dl 0
loc 57
rs 0

How to fix   Long Method    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\PgSql\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
     * @param string $table
18
     *
19
     * @return array
20
     */
21
    private function queryStatus(string $table = '')
22
    {
23
        $query = "SELECT c.relname AS \"Name\", CASE c.relkind " .
24
            "WHEN 'r' THEN 'table' WHEN 'm' THEN 'materialized view' ELSE 'view' END AS \"Engine\", " .
25
            "pg_relation_size(c.oid) AS \"Data_length\", " .
26
            "pg_total_relation_size(c.oid) - pg_relation_size(c.oid) AS \"Index_length\", " .
27
            "obj_description(c.oid, 'pg_class') AS \"Comment\", " .
28
            ($this->driver->minVersion(12) ? "''" : "CASE WHEN c.relhasoids THEN 'oid' ELSE '' END") .
29
            " AS \"Oid\", c.reltuples as \"Rows\", n.nspname FROM pg_class c " .
30
            "JOIN pg_namespace n ON(n.nspname = current_schema() AND n.oid = c.relnamespace) " .
31
            "WHERE relkind IN ('r', 'm', 'v', 'f', 'p') " .
32
            ($table != "" ? "AND relname = " . $this->driver->quote($table) : "ORDER BY relname");
33
        return $this->driver->rows($query);
34
    }
35
36
    /**
37
     * @param array $row
38
     *
39
     * @return TableEntity
40
     */
41
    private function makeStatus(array $row)
42
    {
43
        $status = new TableEntity($row['Name']);
44
        $status->engine = $row['Engine'];
45
        $status->schema = $row['nspname'];
46
        $status->dataLength = $row['Data_length'];
47
        $status->indexLength = $row['Index_length'];
48
        $status->oid = $row['Oid'];
49
        $status->rows = $row['Rows'];
50
        $status->comment = $row['Comment'];
51
52
        return $status;
53
    }
54
55
    /**
56
     * @inheritDoc
57
     */
58
    public function tableStatus(string $table, bool $fast = false)
59
    {
60
        $rows = $this->queryStatus($table);
61
        if (!($row = reset($rows))) {
62
            return null;
63
        }
64
        return $this->makeStatus($row);
65
    }
66
67
    /**
68
     * @inheritDoc
69
     */
70
    public function tablesStatuses(bool $fast = false)
0 ignored issues
show
Unused Code introduced by
The parameter $fast is not used and could be removed. ( Ignorable by Annotation )

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

70
    public function tablesStatuses(/** @scrutinizer ignore-unused */ bool $fast = false)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
71
    {
72
        $tables = [];
73
        $rows = $this->queryStatus();
74
        foreach ($rows as $row) {
75
            $tables[$row["Name"]] = $this->makeStatus($row);
76
        }
77
        return $tables;
78
    }
79
80
    /**
81
     * @inheritDoc
82
     */
83
    public function isView(TableEntity $tableStatus)
84
    {
85
        return in_array($tableStatus->engine, ["view", "materialized view"]);
86
    }
87
88
    /**
89
     * @inheritDoc
90
     */
91
    public function supportForeignKeys(TableEntity $tableStatus)
92
    {
93
        return true;
94
    }
95
96
97
    /**
98
     * Get the primary key of a table
99
     * Same as indexes(), but the columns of the primary key are returned in a array
100
     *
101
     * @param string $table
102
     *
103
     * @return array
104
     */
105
    private function primaryKeyColumns(string $table)
106
    {
107
        $indexes = [];
108
        $table_oid = $this->connection->result("SELECT oid FROM pg_class WHERE " .
109
            "relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " .
110
            "AND relname = " . $this->driver->quote($table));
111
        $columns = $this->driver->keyValues("SELECT attnum, attname FROM pg_attribute WHERE " .
112
            "attrelid = $table_oid AND attnum > 0");
113
        foreach ($this->driver->rows("SELECT relname, indisunique::int, indisprimary::int, indkey, " .
114
            "indoption, (indpred IS NOT NULL)::int as indispartial FROM pg_index i, pg_class ci " .
115
            "WHERE i.indrelid = $table_oid AND ci.oid = i.indexrelid") as $row)
116
        {
117
            // $relname = $row["relname"];
118
            if ($row["indisprimary"]) {
119
                foreach (explode(" ", $row["indkey"]) as $indkey) {
120
                    $indexes[] = $columns[$indkey];
121
                }
122
            }
123
        }
124
        return $indexes;
125
    }
126
127
    /**
128
     * @inheritDoc
129
     */
130
    public function fields(string $table)
131
    {
132
        $fields = [];
133
        $aliases = [
134
            'timestamp without time zone' => 'timestamp',
135
            'timestamp with time zone' => 'timestamptz',
136
        ];
137
138
        // Primary keys
139
        $primaryKeyColumns = $this->primaryKeyColumns($table);
140
141
        $identity_column = $this->driver->minVersion(10) ? 'a.attidentity' : '0';
142
        $query = "SELECT a.attname AS field, format_type(a.atttypid, a.atttypmod) AS full_type, " .
143
            "pg_get_expr(d.adbin, d.adrelid) AS default, a.attnotnull::int, " .
144
            "col_description(c.oid, a.attnum) AS comment, $identity_column AS identity FROM pg_class c " .
145
            "JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_attribute a ON c.oid = a.attrelid " .
146
            "LEFT JOIN pg_attrdef d ON c.oid = d.adrelid AND a.attnum = d.adnum WHERE c.relname = " .
147
            $this->driver->quote($table) .
148
            " AND n.nspname = current_schema() AND NOT a.attisdropped AND a.attnum > 0 ORDER BY a.attnum";
149
        foreach ($this->driver->rows($query) as $row)
150
        {
151
            $field = new TableFieldEntity();
152
153
            $field->name = $row["field"];
154
            $field->primary = \in_array($field->name, $primaryKeyColumns);
155
            $field->fullType = $row["full_type"];
156
            $field->default = $row["default"];
157
            $field->comment = $row["comment"];
158
            //! No collation, no info about primary keys
159
            preg_match('~([^([]+)(\((.*)\))?([a-z ]+)?((\[[0-9]*])*)$~', $field->fullType, $match);
160
            list(, $type, $length, $field->length, $addon, $array) = $match;
161
            $field->length .= $array;
162
            $check_type = $type . $addon;
163
            if (isset($aliases[$check_type])) {
164
                $field->type = $aliases[$check_type];
165
                $field->fullType = $field->type . $length . $array;
166
            } else {
167
                $field->type = $type;
168
                $field->fullType = $field->type . $length . $addon . $array;
169
            }
170
            if (in_array($row['identity'], ['a', 'd'])) {
171
                $field->default = 'GENERATED ' . ($row['identity'] == 'd' ? 'BY DEFAULT' : 'ALWAYS') . ' AS IDENTITY';
172
            }
173
            $field->null = !$row["attnotnull"];
174
            $field->autoIncrement = $row['identity'] || preg_match('~^nextval\(~i', $row["default"]);
175
            $field->privileges = ["insert" => 1, "select" => 1, "update" => 1];
176
            if (preg_match('~(.+)::[^,)]+(.*)~', $row["default"], $match)) {
177
                $match1 = $match[1] ?? '';
178
                $match10 = $match1[0] ?? '';
179
                $match2 = $match[2] ?? '';
180
                $field->default = ($match1 == "NULL" ? null :
181
                    (($match10 == "'" ? $this->driver->unescapeId($match1) : $match1) . $match2));
182
            }
183
184
            $fields[$field->name] = $field;
185
        }
186
        return $fields;
187
    }
188
189
    /**
190
     * @inheritDoc
191
     */
192
    public function indexes(string $table, ConnectionInterface $connection = null)
193
    {
194
        if (!$connection) {
195
            $connection = $this->connection;
196
        }
197
        $indexes = [];
198
        $table_oid = $connection->result("SELECT oid FROM pg_class WHERE " .
199
            "relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " .
200
            "AND relname = " . $this->driver->quote($table));
201
        $columns = $this->driver->keyValues("SELECT attnum, attname FROM pg_attribute WHERE " .
202
            "attrelid = $table_oid AND attnum > 0", $connection);
203
        foreach ($this->driver->rows("SELECT relname, indisunique::int, indisprimary::int, indkey, " .
204
            "indoption, (indpred IS NOT NULL)::int as indispartial FROM pg_index i, pg_class ci " .
205
            "WHERE i.indrelid = $table_oid AND ci.oid = i.indexrelid", $connection) as $row)
206
        {
207
            $index = new IndexEntity();
208
209
            $relname = $row["relname"];
210
            $index->type = ($row["indispartial"] ? "INDEX" :
211
                ($row["indisprimary"] ? "PRIMARY" : ($row["indisunique"] ? "UNIQUE" : "INDEX")));
212
            $index->columns = [];
213
            foreach (explode(" ", $row["indkey"]) as $indkey) {
214
                $index->columns[] = $columns[$indkey];
215
            }
216
            $index->descs = [];
217
            foreach (explode(" ", $row["indoption"]) as $indoption) {
218
                $index->descs[] = ($indoption & 1 ? '1' : null); // 1 - INDOPTION_DESC
219
            }
220
            $index->lengths = [];
221
222
            $indexes[$relname] = $index;
223
        }
224
        return $indexes;
225
    }
226
227
    /**
228
     * @inheritDoc
229
     */
230
    public function foreignKeys(string $table)
231
    {
232
        $foreignKeys = [];
233
        $onActions = $this->driver->actions();
234
        $query = "SELECT conname, condeferrable::int AS deferrable, pg_get_constraintdef(oid) " .
235
            "AS definition FROM pg_constraint WHERE conrelid = (SELECT pc.oid FROM pg_class AS pc " .
236
            "INNER JOIN pg_namespace AS pn ON (pn.oid = pc.relnamespace) WHERE pc.relname = " .
237
            $this->driver->quote($table) .
238
            " AND pn.nspname = current_schema()) AND contype = 'f'::char ORDER BY conkey, conname";
239
        foreach ($this->driver->rows($query) as $row) {
240
            if (preg_match('~FOREIGN KEY\s*\((.+)\)\s*REFERENCES (.+)\((.+)\)(.*)$~iA', $row['definition'], $match)) {
241
                $match1 = $match[1] ?? '';
242
                $match2 = $match[2] ?? '';
243
                $match3 = $match[3] ?? '';
244
                $match4 = $match[4] ?? '';
245
                $match11 = '';
246
247
                $foreignKey = new ForeignKeyEntity();
248
249
                $foreignKey->source = array_map('trim', explode(',', $match1));
250
                $foreignKey->target = array_map('trim', explode(',', $match3));
251
                $foreignKey->onDelete = preg_match("~ON DELETE ($onActions)~", $match4, $match10) ? $match11 : 'NO ACTION';
252
                $foreignKey->onUpdate = preg_match("~ON UPDATE ($onActions)~", $match4, $match10) ? $match11 : 'NO ACTION';
253
254
                if (preg_match('~^(("([^"]|"")+"|[^"]+)\.)?"?("([^"]|"")+"|[^"]+)$~', $match2, $match10)) {
255
                    // $match11 = $match10[1] ?? '';
256
                    $match12 = $match10[2] ?? '';
257
                    // $match13 = $match10[3] ?? '';
258
                    $match14 = $match10[4] ?? '';
259
                    $foreignKey->schema = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match12));
260
                    $foreignKey->table = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match14));
261
                }
262
263
                $foreignKeys[$row['conname']] = $foreignKey;
264
            }
265
        }
266
        return $foreignKeys;
267
    }
268
269
    /**
270
     * @inheritDoc
271
     */
272
    public function alterTable(string $table, string $name, array $fields, array $foreign,
273
        string $comment, string $engine, string $collation, int $autoIncrement, string $partitioning)
274
    {
275
        $alter = [];
276
        $queries = [];
277
        if ($table != "" && $table != $name) {
278
            $queries[] = "ALTER TABLE " . $this->driver->table($table) . " RENAME TO " . $this->driver->table($name);
279
        }
280
        foreach ($fields as $field) {
281
            $column = $this->driver->escapeId($field[0]);
282
            $val = $field[1];
283
            if (!$val) {
284
                $alter[] = "DROP $column";
285
            } else {
286
                $val5 = $val[5];
287
                unset($val[5]);
288
                if ($field[0] == "") {
289
                    if (isset($val[6])) { // auto increment
290
                        $val[1] = ($val[1] == " bigint" ? " big" : ($val[1] == " smallint" ? " small" : " ")) . "serial";
291
                    }
292
                    $alter[] = ($table != "" ? "ADD " : "  ") . implode($val);
293
                    if (isset($val[6])) {
294
                        $alter[] = ($table != "" ? "ADD" : " ") . " PRIMARY KEY ($val[0])";
295
                    }
296
                } else {
297
                    if ($column != $val[0]) {
298
                        $queries[] = "ALTER TABLE " . $this->driver->table($name) . " RENAME $column TO $val[0]";
299
                    }
300
                    $alter[] = "ALTER $column TYPE$val[1]";
301
                    if (!$val[6]) {
302
                        $alter[] = "ALTER $column " . ($val[3] ? "SET$val[3]" : "DROP DEFAULT");
303
                        $alter[] = "ALTER $column " . ($val[2] == " NULL" ? "DROP NOT" : "SET") . $val[2];
304
                    }
305
                }
306
                if ($field[0] != "" || $val5 != "") {
307
                    $queries[] = "COMMENT ON COLUMN " . $this->driver->table($name) . ".$val[0] IS " . ($val5 != "" ? substr($val5, 9) : "''");
308
                }
309
            }
310
        }
311
        $alter = array_merge($alter, $foreign);
312
        if ($table == "") {
313
            array_unshift($queries, "CREATE TABLE " . $this->driver->table($name) . " (\n" . implode(",\n", $alter) . "\n)");
314
        } elseif (!empty($alter)) {
315
            array_unshift($queries, "ALTER TABLE " . $this->driver->table($table) . "\n" . implode(",\n", $alter));
316
        }
317
        if ($table != "" || $comment != "") {
318
            $queries[] = "COMMENT ON TABLE " . $this->driver->table($name) . " IS " . $this->driver->quote($comment);
319
        }
320
        if ($autoIncrement != "") {
321
            //! $queries[] = "SELECT setval(pg_get_serial_sequence(" . $this->driver->quote($name) . ", ), $autoIncrement)";
322
        }
323
        foreach ($queries as $query) {
324
            if (!$this->driver->execute($query)) {
325
                return false;
326
            }
327
        }
328
        return true;
329
    }
330
331
    /**
332
     * @inheritDoc
333
     */
334
    public function alterIndexes(string $table, array $alter)
335
    {
336
        $create = [];
337
        $drop = [];
338
        $queries = [];
339
        foreach ($alter as $val) {
340
            if ($val[0] != "INDEX") {
341
                //! descending UNIQUE indexes results in syntax error
342
                $create[] = (
343
                    $val[2] == "DROP" ? "\nDROP CONSTRAINT " . $this->driver->escapeId($val[1]) :
344
                    "\nADD" . ($val[1] != "" ? " CONSTRAINT " . $this->driver->escapeId($val[1]) : "") .
345
                    " $val[0] " . ($val[0] == "PRIMARY" ? "KEY " : "") . "(" . implode(", ", $val[2]) . ")"
346
                );
347
            } elseif ($val[2] == "DROP") {
348
                $drop[] = $this->driver->escapeId($val[1]);
349
            } else {
350
                $queries[] = "CREATE INDEX " . $this->driver->escapeId($val[1] != "" ? $val[1] : uniqid($table . "_")) .
351
                    " ON " . $this->driver->table($table) . " (" . implode(", ", $val[2]) . ")";
352
            }
353
        }
354
        if ($create) {
355
            array_unshift($queries, "ALTER TABLE " . $this->driver->table($table) . implode(",", $create));
356
        }
357
        if ($drop) {
358
            array_unshift($queries, "DROP INDEX " . implode(", ", $drop));
359
        }
360
        foreach ($queries as $query) {
361
            if (!$this->driver->execute($query)) {
362
                return false;
363
            }
364
        }
365
        return true;
366
    }
367
368
    /**
369
     * @inheritDoc
370
     */
371
    public function trigger(string $trigger/*, $table = null*/)
372
    {
373
        if ($trigger == "") {
374
            return ["Statement" => "EXECUTE PROCEDURE ()"];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('Statement'...'EXECUTE PROCEDURE ()') 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...
375
        }
376
        // if ($table === null) {
377
            $table = $this->util->input()->getTable();
378
        // }
379
        $query = 'SELECT t.trigger_name AS "Trigger", t.action_timing AS "Timing", ' .
380
            '(SELECT STRING_AGG(event_manipulation, \' OR \') FROM information_schema.triggers ' .
381
            'WHERE event_object_table = t.event_object_table AND trigger_name = t.trigger_name ) AS "Events", ' .
382
            't.event_manipulation AS "Event", \'FOR EACH \' || t.action_orientation AS "Type", ' .
383
            't.action_statement AS "Statement" FROM information_schema.triggers t WHERE t.event_object_table = ' .
384
            $this->driver->quote($table) . ' AND t.trigger_name = ' . $this->driver->quote($trigger);
385
        $rows = $this->driver->rows($query);
386
        return reset($rows);
387
    }
388
389
    /**
390
     * @inheritDoc
391
     */
392
    public function triggers(string $table)
393
    {
394
        $triggers = [];
395
        $query = "SELECT * FROM information_schema.triggers WHERE trigger_schema = current_schema() " .
396
            "AND event_object_table = " . $this->driver->quote($table);
397
        foreach ($this->driver->rows($query) as $row) {
398
            $triggers[$row["trigger_name"]] = new Trigger($row["action_timing"], $row["event_manipulation"]);
0 ignored issues
show
Bug introduced by
The type Lagdo\DbAdmin\Driver\PgSql\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...
399
        }
400
        return $triggers;
401
    }
402
403
    /**
404
     * @inheritDoc
405
     */
406
    public function triggerOptions()
407
    {
408
        return [
409
            "Timing" => ["BEFORE", "AFTER"],
410
            "Event" => ["INSERT", "UPDATE", "DELETE"],
411
            "Type" => ["FOR EACH ROW", "FOR EACH STATEMENT"],
412
        ];
413
    }
414
415
    /**
416
     * @inheritDoc
417
     */
418
    public function tableHelp(string $name)
419
    {
420
        $links = array(
421
            "information_schema" => "infoschema",
422
            "pg_catalog" => "catalog",
423
        );
424
        $link = $links[$this->driver->schema()];
425
        if ($link) {
426
            return "$link-" . str_replace("_", "-", $name) . ".html";
427
        }
428
    }
429
}
430