Passed
Push — main ( fc777d...bee40e )
by Thierry
03:20 queued 01:40
created

Table::trigger()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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