Passed
Push — main ( 0976a7...b2ddc1 )
by Thierry
01:51
created

Table::getFieldDefault()   B

Complexity

Conditions 7
Paths 36

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 10
c 1
b 0
f 0
nc 36
nop 1
dl 0
loc 15
rs 8.8333
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");
0 ignored issues
show
Bug introduced by
The method quote() does not exist on Lagdo\DbAdmin\Driver\DriverInterface. Did you maybe mean quoteBinary()? ( Ignorable by Annotation )

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

33
            ($table != "" ? "AND relname = " . $this->driver->/** @scrutinizer ignore-call */ quote($table) : "ORDER BY relname");

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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
    private function getFieldDefault(array $row)
167
    {
168
        $default = $row["default"];
169
        if ($row['identity'] === 'a') {
170
            $default = 'GENERATED ALWAYS AS IDENTITY';
171
        }
172
        if ($row['identity'] === 'd') {
173
            $default = 'GENERATED BY DEFAULT AS IDENTITY';
174
        }
175
        if (preg_match('~(.+)::[^,)]+(.*)~', $row["default"], $match)) {
176
            $match = array_pad($match, 3, '');
177
            $default = ($match[1] == "NULL") ? null : ((!empty($match[1]) && $match[1][0] == "'") ?
178
                $this->driver->unescapeId($match[1]) : $match[1]) . $match[2];
179
        }
180
        return $default;
181
    }
182
183
    private function getFieldTypes(array $row)
184
    {
185
        $aliases = [
186
            'timestamp without time zone' => 'timestamp',
187
            'timestamp with time zone' => 'timestamptz',
188
        ];
189
        preg_match('~([^([]+)(\((.*)\))?([a-z ]+)?((\[[0-9]*])*)$~', $row["full_type"], $match);
190
        list(, $type, $_length, $length, $addon, $array) = $match;
191
        $length .= $array;
192
        $checkType = $type . $addon;
193
        if (isset($aliases[$checkType])) {
194
            $type = $aliases[$checkType];
195
            $fullType = $type . $_length . $array;
196
            return [$length, $type, $fullType];
197
        }
198
        $fullType = $type . $_length . $addon . $array;
199
        return [$length, $type, $fullType];
200
    }
201
202
    /**
203
     * @param array $row
204
     * @param array $primaryKeyColumns
205
     *
206
     * @return TableFieldEntity
207
     */
208
    private function makeFieldEntity(array $row, array $primaryKeyColumns)
209
    {
210
        $field = new TableFieldEntity();
211
212
        $field->name = $row["field"];
213
        $field->primary = \in_array($field->name, $primaryKeyColumns);
214
        $field->fullType = $row["full_type"];
215
        $field->default = $this->getFieldDefault($row);
216
        $field->comment = $row["comment"];
217
        //! No collation, no info about primary keys
218
        list($field->length, $field->type, $field->fullType) = $this->getFieldTypes($row);
219
        $field->null = !$row["attnotnull"];
220
        $field->autoIncrement = $row['identity'] || preg_match('~^nextval\(~i', $row["default"]);
221
        $field->privileges = ["insert" => 1, "select" => 1, "update" => 1];
222
        return $field;
223
    }
224
225
    /**
226
     * @inheritDoc
227
     */
228
    public function fields(string $table)
229
    {
230
        $fields = [];
231
232
        // Primary keys
233
        $primaryKeyColumns = $this->primaryKeyColumns($table);
234
235
        $identity_column = $this->driver->minVersion(10) ? 'a.attidentity' : '0';
236
        $query = "SELECT a.attname AS field, format_type(a.atttypid, a.atttypmod) AS full_type, " .
237
            "pg_get_expr(d.adbin, d.adrelid) AS default, a.attnotnull::int, " .
238
            "col_description(c.oid, a.attnum) AS comment, $identity_column AS identity FROM pg_class c " .
239
            "JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_attribute a ON c.oid = a.attrelid " .
240
            "LEFT JOIN pg_attrdef d ON c.oid = d.adrelid AND a.attnum = d.adnum WHERE c.relname = " .
241
            $this->driver->quote($table) .
242
            " AND n.nspname = current_schema() AND NOT a.attisdropped AND a.attnum > 0 ORDER BY a.attnum";
243
        foreach ($this->driver->rows($query) as $row)
244
        {
245
            $fields[$row["field"]] = $this->makeFieldEntity($row, $primaryKeyColumns);
246
        }
247
        return $fields;
248
    }
249
250
    /**
251
     * @param array $row
252
     *
253
     * @return string
254
     */
255
    private function getIndexType(array $row)
256
    {
257
        if ($row['indispartial']) {
258
            return 'INDEX';
259
        }
260
        if ($row['indisprimary']) {
261
            return 'PRIMARY';
262
        }
263
        if ($row['indisunique']) {
264
            return 'UNIQUE';
265
        }
266
        return 'INDEX';
267
    }
268
269
    /**
270
     * @param array $row
271
     * @param array $columns
272
     *
273
     * @return IndexEntity
274
     */
275
    private function makeIndexEntity(array $row, array $columns)
276
    {
277
        $index = new IndexEntity();
278
279
        $index->type = $this->getIndexType($row);
280
        $index->columns = [];
281
        foreach (explode(' ', $row['indkey']) as $indkey) {
282
            $index->columns[] = $columns[$indkey];
283
        }
284
        $index->descs = [];
285
        foreach (explode(' ', $row['indoption']) as $indoption) {
286
            $index->descs[] = ($indoption & 1 ? '1' : null); // 1 - INDOPTION_DESC
287
        }
288
        $index->lengths = [];
289
290
        return $index;
291
    }
292
293
    /**
294
     * @inheritDoc
295
     */
296
    public function indexes(string $table, ConnectionInterface $connection = null)
297
    {
298
        if (!$connection) {
299
            $connection = $this->connection;
300
        }
301
        $indexes = [];
302
        $table_oid = $connection->result("SELECT oid FROM pg_class WHERE " .
303
            "relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " .
304
            "AND relname = " . $this->driver->quote($table));
305
        $columns = $this->driver->keyValues("SELECT attnum, attname FROM pg_attribute WHERE " .
306
            "attrelid = $table_oid AND attnum > 0", $connection);
307
        $query = "SELECT relname, indisunique::int, indisprimary::int, indkey, indoption, " .
308
            "(indpred IS NOT NULL)::int as indispartial FROM pg_index i, pg_class ci " .
309
            "WHERE i.indrelid = $table_oid AND ci.oid = i.indexrelid";
310
        foreach ($this->driver->rows($query, $connection) as $row)
311
        {
312
            $indexes[$row["relname"]] = $this->makeIndexEntity($row, $columns);
313
        }
314
        return $indexes;
315
    }
316
317
    /**
318
     * @param array $row
319
     *
320
     * @return ForeignKeyEntity
321
     */
322
    private function makeForeignKeyEntity(array $row)
323
    {
324
        if (!preg_match('~FOREIGN KEY\s*\((.+)\)\s*REFERENCES (.+)\((.+)\)(.*)$~iA', $row['definition'], $match)) {
325
            return null;
326
        }
327
        $onActions = $this->driver->actions();
328
        $match = array_pad($match, 5, '');
329
330
        $foreignKey = new ForeignKeyEntity();
331
332
        $foreignKey->source = array_map('trim', explode(',', $match[1]));
333
        $foreignKey->target = array_map('trim', explode(',', $match[3]));
334
335
        if (preg_match('~^(("([^"]|"")+"|[^"]+)\.)?"?("([^"]|"")+"|[^"]+)$~', $match[2], $match2)) {
336
            $match2 = array_pad($match2, 5, '');
337
            $foreignKey->schema = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match2[2]));
338
            $foreignKey->table = str_replace('""', '"', preg_replace('~^"(.+)"$~', '\1', $match2[4]));
339
        }
340
341
        $foreignKey->onDelete = preg_match("~ON DELETE ($onActions)~", $match[4], $match2) ? $match2[1] : 'NO ACTION';
342
        $foreignKey->onUpdate = preg_match("~ON UPDATE ($onActions)~", $match[4], $match2) ? $match2[1] : 'NO ACTION';
343
344
        return $foreignKey;
345
    }
346
347
    /**
348
     * @inheritDoc
349
     */
350
    public function foreignKeys(string $table)
351
    {
352
        $foreignKeys = [];
353
        $query = "SELECT conname, condeferrable::int AS deferrable, pg_get_constraintdef(oid) " .
354
            "AS definition FROM pg_constraint WHERE conrelid = (SELECT pc.oid FROM pg_class AS pc " .
355
            "INNER JOIN pg_namespace AS pn ON (pn.oid = pc.relnamespace) WHERE pc.relname = " .
356
            $this->driver->quote($table) .
357
            " AND pn.nspname = current_schema()) AND contype = 'f'::char ORDER BY conkey, conname";
358
        foreach ($this->driver->rows($query) as $row) {
359
            $foreignKey = $this->makeForeignKeyEntity($row);
360
            if ($foreignKey !== null) {
361
                $foreignKeys[$row['conname']] = $foreignKey;
362
            }
363
        }
364
        return $foreignKeys;
365
    }
366
367
    /**
368
     * @inheritDoc
369
     */
370
    public function trigger(string $name, string $table = '')
371
    {
372
        if ($name == '') {
373
            return new TriggerEntity('', '', 'EXECUTE PROCEDURE ()');
374
        }
375
        if ($table === '') {
376
            $table = $this->util->input()->getTable();
377
        }
378
        $query = 'SELECT t.trigger_name AS "Trigger", t.action_timing AS "Timing", ' .
379
            '(SELECT STRING_AGG(event_manipulation, \' OR \') FROM information_schema.triggers ' .
380
            'WHERE event_object_table = t.event_object_table AND trigger_name = t.trigger_name ) AS "Events", ' .
381
            't.event_manipulation AS "Event", \'FOR EACH \' || t.action_orientation AS "Type", ' .
382
            't.action_statement AS "Statement" FROM information_schema.triggers t WHERE t.event_object_table = ' .
383
            $this->driver->quote($table) . ' AND t.trigger_name = ' . $this->driver->quote($name);
384
        $rows = $this->driver->rows($query);
385
        if (!($row = reset($rows))) {
386
            return null;
387
        }
388
        return new TriggerEntity($row['Timing'], $row['Event'], $row['Statement'], '', $row['Trigger']);
389
    }
390
391
    /**
392
     * @inheritDoc
393
     */
394
    public function triggers(string $table)
395
    {
396
        $triggers = [];
397
        $query = "SELECT * FROM information_schema.triggers WHERE trigger_schema = current_schema() " .
398
            "AND event_object_table = " . $this->driver->quote($table);
399
        foreach ($this->driver->rows($query) as $row) {
400
            $triggers[$row["trigger_name"]] = new TriggerEntity($row["action_timing"],
401
                $row["event_manipulation"], '', '', $row["trigger_name"]);
402
        }
403
        return $triggers;
404
    }
405
406
    /**
407
     * @inheritDoc
408
     */
409
    public function triggerOptions()
410
    {
411
        return [
412
            "Timing" => ["BEFORE", "AFTER"],
413
            "Event" => ["INSERT", "UPDATE", "DELETE"],
414
            "Type" => ["FOR EACH ROW", "FOR EACH STATEMENT"],
415
        ];
416
    }
417
418
    /**
419
     * @inheritDoc
420
     */
421
    public function tableHelp(string $name)
422
    {
423
        $links = [
424
            "information_schema" => "infoschema",
425
            "pg_catalog" => "catalog",
426
        ];
427
        $link = $links[$this->driver->schema()];
428
        if ($link) {
429
            return "$link-" . str_replace("_", "-", $name) . ".html";
430
        }
431
    }
432
}
433