Passed
Push — main ( c996bf...50ff31 )
by Thierry
01:51
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\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