Passed
Branch main (a6702e)
by Thierry
04:09 queued 02:04
created

Table   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 186
c 4
b 0
f 0
dl 0
loc 356
rs 5.5199
wmc 56

16 Methods

Rating   Name   Duplication   Size   Complexity  
A queryStatus() 0 13 3
A makeStatus() 0 12 1
A tableStatus() 0 7 2
A isView() 0 3 1
A trigger() 0 19 4
A primaryKeyColumns() 0 20 4
A referencableTables() 0 20 6
A triggerOptions() 0 6 1
A tableNames() 0 8 2
A tableHelp() 0 9 2
B fields() 0 57 10
A triggers() 0 10 2
A tableStatuses() 0 8 2
B indexes() 0 33 9
A supportForeignKeys() 0 3 1
B foreignKeys() 0 37 6

How to fix   Complexity   

Complex Class

Complex classes like Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Table, and based on these observations, apply Extract Interface, too.

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
use Lagdo\DbAdmin\Driver\Entity\TriggerEntity;
10
11
use Lagdo\DbAdmin\Driver\Db\ConnectionInterface;
12
13
use Lagdo\DbAdmin\Driver\Db\Table as AbstractTable;
14
15
class Table extends AbstractTable
16
{
17
    /**
18
     * @param string $table
19
     *
20
     * @return array
21
     */
22
    private function queryStatus(string $table = '')
23
    {
24
        $query = "SELECT c.relname AS \"Name\", CASE c.relkind " .
25
            "WHEN 'r' THEN 'table' WHEN 'm' THEN 'materialized view' ELSE 'view' END AS \"Engine\", " .
26
            "pg_relation_size(c.oid) AS \"Data_length\", " .
27
            "pg_total_relation_size(c.oid) - pg_relation_size(c.oid) AS \"Index_length\", " .
28
            "obj_description(c.oid, 'pg_class') AS \"Comment\", " .
29
            ($this->driver->minVersion(12) ? "''" : "CASE WHEN c.relhasoids THEN 'oid' ELSE '' END") .
30
            " AS \"Oid\", c.reltuples as \"Rows\", n.nspname FROM pg_class c " .
31
            "JOIN pg_namespace n ON(n.nspname = current_schema() AND n.oid = c.relnamespace) " .
32
            "WHERE relkind IN ('r', 'm', 'v', 'f', 'p') " .
33
            ($table != "" ? "AND relname = " . $this->driver->quote($table) : "ORDER BY relname");
34
        return $this->driver->rows($query);
35
    }
36
37
    /**
38
     * @param array $row
39
     *
40
     * @return TableEntity
41
     */
42
    private function makeStatus(array $row)
43
    {
44
        $status = new TableEntity($row['Name']);
45
        $status->engine = $row['Engine'];
46
        $status->schema = $row['nspname'];
47
        $status->dataLength = $row['Data_length'];
48
        $status->indexLength = $row['Index_length'];
49
        $status->oid = $row['Oid'];
50
        $status->rows = $row['Rows'];
51
        $status->comment = $row['Comment'];
52
53
        return $status;
54
    }
55
56
    /**
57
     * @inheritDoc
58
     */
59
    public function tableStatus(string $table, bool $fast = false)
60
    {
61
        $rows = $this->queryStatus($table);
62
        if (!($row = reset($rows))) {
63
            return null;
64
        }
65
        return $this->makeStatus($row);
66
    }
67
68
    /**
69
     * @inheritDoc
70
     */
71
    public function tableStatuses(bool $fast = false)
72
    {
73
        $tables = [];
74
        $rows = $this->queryStatus();
75
        foreach ($rows as $row) {
76
            $tables[$row["Name"]] = $this->makeStatus($row);
77
        }
78
        return $tables;
79
    }
80
81
    /**
82
     * @inheritDoc
83
     */
84
    public function tableNames()
85
    {
86
        $tables = [];
87
        $rows = $this->queryStatus();
88
        foreach ($rows as $row) {
89
            $tables[] = $row["Name"];
90
        }
91
        return $tables;
92
    }
93
94
    /**
95
     * @inheritDoc
96
     */
97
    public function isView(TableEntity $tableStatus)
98
    {
99
        return in_array($tableStatus->engine, ["view", "materialized view"]);
100
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105
    public function supportForeignKeys(TableEntity $tableStatus)
106
    {
107
        return true;
108
    }
109
110
    /**
111
     * @inheritDoc
112
     */
113
    public function referencableTables(string $table)
114
    {
115
        $fields = []; // table_name => [field]
116
        foreach ($this->tableNames() as $tableName) {
117
            if ($tableName === $table) {
118
                continue;
119
            }
120
            foreach ($this->fields($tableName) as $field) {
121
                if ($field->primary) {
122
                    if (!isset($fields[$tableName])) {
123
                        $fields[$tableName] = $field;
124
                    } else {
125
                        // No multi column primary key
126
                        $fields[$tableName] = null;
127
                    }
128
                }
129
            }
130
        }
131
        return array_filter($fields, function($field) {
132
            return $field !== null;
133
        });
134
    }
135
136
    /**
137
     * Get the primary key of a table
138
     * Same as indexes(), but the columns of the primary key are returned in a array
139
     *
140
     * @param string $table
141
     *
142
     * @return array
143
     */
144
    private function primaryKeyColumns(string $table)
145
    {
146
        $indexes = [];
147
        $table_oid = $this->connection->result("SELECT oid FROM pg_class WHERE " .
148
            "relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " .
149
            "AND relname = " . $this->driver->quote($table));
150
        $columns = $this->driver->keyValues("SELECT attnum, attname FROM pg_attribute WHERE " .
151
            "attrelid = $table_oid AND attnum > 0");
152
        foreach ($this->driver->rows("SELECT relname, indisunique::int, indisprimary::int, indkey, " .
153
            "indoption, (indpred IS NOT NULL)::int as indispartial FROM pg_index i, pg_class ci " .
154
            "WHERE i.indrelid = $table_oid AND ci.oid = i.indexrelid") as $row)
155
        {
156
            // $relname = $row["relname"];
157
            if ($row["indisprimary"]) {
158
                foreach (explode(" ", $row["indkey"]) as $indkey) {
159
                    $indexes[] = $columns[$indkey];
160
                }
161
            }
162
        }
163
        return $indexes;
164
    }
165
166
    /**
167
     * @inheritDoc
168
     */
169
    public function fields(string $table)
170
    {
171
        $fields = [];
172
        $aliases = [
173
            'timestamp without time zone' => 'timestamp',
174
            'timestamp with time zone' => 'timestamptz',
175
        ];
176
177
        // Primary keys
178
        $primaryKeyColumns = $this->primaryKeyColumns($table);
179
180
        $identity_column = $this->driver->minVersion(10) ? 'a.attidentity' : '0';
181
        $query = "SELECT a.attname AS field, format_type(a.atttypid, a.atttypmod) AS full_type, " .
182
            "pg_get_expr(d.adbin, d.adrelid) AS default, a.attnotnull::int, " .
183
            "col_description(c.oid, a.attnum) AS comment, $identity_column AS identity FROM pg_class c " .
184
            "JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_attribute a ON c.oid = a.attrelid " .
185
            "LEFT JOIN pg_attrdef d ON c.oid = d.adrelid AND a.attnum = d.adnum WHERE c.relname = " .
186
            $this->driver->quote($table) .
187
            " AND n.nspname = current_schema() AND NOT a.attisdropped AND a.attnum > 0 ORDER BY a.attnum";
188
        foreach ($this->driver->rows($query) as $row)
189
        {
190
            $field = new TableFieldEntity();
191
192
            $field->name = $row["field"];
193
            $field->primary = \in_array($field->name, $primaryKeyColumns);
194
            $field->fullType = $row["full_type"];
195
            $field->default = $row["default"];
196
            $field->comment = $row["comment"];
197
            //! No collation, no info about primary keys
198
            preg_match('~([^([]+)(\((.*)\))?([a-z ]+)?((\[[0-9]*])*)$~', $field->fullType, $match);
199
            list(, $type, $length, $field->length, $addon, $array) = $match;
200
            $field->length .= $array;
201
            $check_type = $type . $addon;
202
            if (isset($aliases[$check_type])) {
203
                $field->type = $aliases[$check_type];
204
                $field->fullType = $field->type . $length . $array;
205
            } else {
206
                $field->type = $type;
207
                $field->fullType = $field->type . $length . $addon . $array;
208
            }
209
            if (in_array($row['identity'], ['a', 'd'])) {
210
                $field->default = 'GENERATED ' . ($row['identity'] == 'd' ? 'BY DEFAULT' : 'ALWAYS') . ' AS IDENTITY';
211
            }
212
            $field->null = !$row["attnotnull"];
213
            $field->autoIncrement = $row['identity'] || preg_match('~^nextval\(~i', $row["default"]);
214
            $field->privileges = ["insert" => 1, "select" => 1, "update" => 1];
215
            if (preg_match('~(.+)::[^,)]+(.*)~', $row["default"], $match)) {
216
                $match1 = $match[1] ?? '';
217
                $match10 = $match1[0] ?? '';
218
                $match2 = $match[2] ?? '';
219
                $field->default = ($match1 == "NULL" ? null :
220
                    (($match10 == "'" ? $this->driver->unescapeId($match1) : $match1) . $match2));
221
            }
222
223
            $fields[$field->name] = $field;
224
        }
225
        return $fields;
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231
    public function indexes(string $table, ConnectionInterface $connection = null)
232
    {
233
        if (!$connection) {
234
            $connection = $this->connection;
235
        }
236
        $indexes = [];
237
        $table_oid = $connection->result("SELECT oid FROM pg_class WHERE " .
238
            "relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " .
239
            "AND relname = " . $this->driver->quote($table));
240
        $columns = $this->driver->keyValues("SELECT attnum, attname FROM pg_attribute WHERE " .
241
            "attrelid = $table_oid AND attnum > 0", $connection);
242
        foreach ($this->driver->rows("SELECT relname, indisunique::int, indisprimary::int, indkey, " .
243
            "indoption, (indpred IS NOT NULL)::int as indispartial FROM pg_index i, pg_class ci " .
244
            "WHERE i.indrelid = $table_oid AND ci.oid = i.indexrelid", $connection) as $row)
245
        {
246
            $index = new IndexEntity();
247
248
            $relname = $row["relname"];
249
            $index->type = ($row["indispartial"] ? "INDEX" :
250
                ($row["indisprimary"] ? "PRIMARY" : ($row["indisunique"] ? "UNIQUE" : "INDEX")));
251
            $index->columns = [];
252
            foreach (explode(" ", $row["indkey"]) as $indkey) {
253
                $index->columns[] = $columns[$indkey];
254
            }
255
            $index->descs = [];
256
            foreach (explode(" ", $row["indoption"]) as $indoption) {
257
                $index->descs[] = ($indoption & 1 ? '1' : null); // 1 - INDOPTION_DESC
258
            }
259
            $index->lengths = [];
260
261
            $indexes[$relname] = $index;
262
        }
263
        return $indexes;
264
    }
265
266
    /**
267
     * @inheritDoc
268
     */
269
    public function foreignKeys(string $table)
270
    {
271
        $foreignKeys = [];
272
        $onActions = $this->driver->actions();
273
        $query = "SELECT conname, condeferrable::int AS deferrable, pg_get_constraintdef(oid) " .
274
            "AS definition FROM pg_constraint WHERE conrelid = (SELECT pc.oid FROM pg_class AS pc " .
275
            "INNER JOIN pg_namespace AS pn ON (pn.oid = pc.relnamespace) WHERE pc.relname = " .
276
            $this->driver->quote($table) .
277
            " AND pn.nspname = current_schema()) AND contype = 'f'::char ORDER BY conkey, conname";
278
        foreach ($this->driver->rows($query) as $row) {
279
            if (preg_match('~FOREIGN KEY\s*\((.+)\)\s*REFERENCES (.+)\((.+)\)(.*)$~iA', $row['definition'], $match)) {
280
                $match1 = $match[1] ?? '';
281
                $match2 = $match[2] ?? '';
282
                $match3 = $match[3] ?? '';
283
                $match4 = $match[4] ?? '';
284
                $match11 = '';
285
286
                $foreignKey = new ForeignKeyEntity();
287
288
                $foreignKey->source = array_map('trim', explode(',', $match1));
289
                $foreignKey->target = array_map('trim', explode(',', $match3));
290
                $foreignKey->onDelete = preg_match("~ON DELETE ($onActions)~", $match4, $match10) ? $match11 : 'NO ACTION';
291
                $foreignKey->onUpdate = preg_match("~ON UPDATE ($onActions)~", $match4, $match10) ? $match11 : 'NO ACTION';
292
293
                if (preg_match('~^(("([^"]|"")+"|[^"]+)\.)?"?("([^"]|"")+"|[^"]+)$~', $match2, $match10)) {
294
                    // $match11 = $match10[1] ?? '';
295
                    $match12 = $match10[2] ?? '';
296
                    // $match13 = $match10[3] ?? '';
297
                    $match14 = $match10[4] ?? '';
298
                    $foreignKey->schema = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match12));
299
                    $foreignKey->table = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match14));
300
                }
301
302
                $foreignKeys[$row['conname']] = $foreignKey;
303
            }
304
        }
305
        return $foreignKeys;
306
    }
307
308
    /**
309
     * @inheritDoc
310
     */
311
    public function trigger(string $name, string $table = '')
312
    {
313
        if ($name == '') {
314
            return new TriggerEntity('', '', 'EXECUTE PROCEDURE ()');
315
        }
316
        if ($table === '') {
317
            $table = $this->util->input()->getTable();
318
        }
319
        $query = 'SELECT t.trigger_name AS "Trigger", t.action_timing AS "Timing", ' .
320
            '(SELECT STRING_AGG(event_manipulation, \' OR \') FROM information_schema.triggers ' .
321
            'WHERE event_object_table = t.event_object_table AND trigger_name = t.trigger_name ) AS "Events", ' .
322
            't.event_manipulation AS "Event", \'FOR EACH \' || t.action_orientation AS "Type", ' .
323
            't.action_statement AS "Statement" FROM information_schema.triggers t WHERE t.event_object_table = ' .
324
            $this->driver->quote($table) . ' AND t.trigger_name = ' . $this->driver->quote($name);
325
        $rows = $this->driver->rows($query);
326
        if (!($row = reset($rows))) {
327
            return null;
328
        }
329
        return new TriggerEntity($row['Timing'], $row['Event'], $row['Statement'], '', $row['Trigger']);
330
    }
331
332
    /**
333
     * @inheritDoc
334
     */
335
    public function triggers(string $table)
336
    {
337
        $triggers = [];
338
        $query = "SELECT * FROM information_schema.triggers WHERE trigger_schema = current_schema() " .
339
            "AND event_object_table = " . $this->driver->quote($table);
340
        foreach ($this->driver->rows($query) as $row) {
341
            $triggers[$row["trigger_name"]] = new TriggerEntity($row["action_timing"],
342
                $row["event_manipulation"], '', '', $row["trigger_name"]);
343
        }
344
        return $triggers;
345
    }
346
347
    /**
348
     * @inheritDoc
349
     */
350
    public function triggerOptions()
351
    {
352
        return [
353
            "Timing" => ["BEFORE", "AFTER"],
354
            "Event" => ["INSERT", "UPDATE", "DELETE"],
355
            "Type" => ["FOR EACH ROW", "FOR EACH STATEMENT"],
356
        ];
357
    }
358
359
    /**
360
     * @inheritDoc
361
     */
362
    public function tableHelp(string $name)
363
    {
364
        $links = [
365
            "information_schema" => "infoschema",
366
            "pg_catalog" => "catalog",
367
        ];
368
        $link = $links[$this->driver->schema()];
369
        if ($link) {
370
            return "$link-" . str_replace("_", "-", $name) . ".html";
371
        }
372
    }
373
}
374