Passed
Branch main (217991)
by Thierry
02:18
created

Table::isView()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Driver\MySql\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 bool $fast
19
     * @param string $table
20
     *
21
     * @return array
22
     */
23
    private function queryStatus(bool $fast, string $table = '')
24
    {
25
        $query = ($fast && $this->driver->minVersion(5)) ?
26
            "SELECT TABLE_NAME AS Name, ENGINE AS Engine, TABLE_COMMENT AS Comment " .
27
            "FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() " .
28
            ($table != "" ? "AND TABLE_NAME = " . $this->driver->quote($table) : "ORDER BY Name") :
29
            "SHOW TABLE STATUS" . ($table != "" ? " LIKE " . $this->driver->quote(addcslashes($table, "%_\\")) : "");
30
        return $this->driver->rows($query);
31
    }
32
33
    /**
34
     * @param array $row
35
     *
36
     * @return TableEntity
37
     */
38
    private function makeStatus(array $row)
39
    {
40
        $status = new TableEntity($row['Name']);
41
        $status->engine = $row['Engine'];
42
        if ($row["Engine"] == "InnoDB") {
43
            // ignore internal comment, unnecessary since MySQL 5.1.21
44
            $status->comment = preg_replace('~(?:(.+); )?InnoDB free: .*~', '\1', $row["Comment"]);
45
        }
46
        // if (!isset($row["Engine"])) {
47
        //     $row["Comment"] = "";
48
        // }
49
50
        return $status;
51
    }
52
53
    /**
54
     * @inheritDoc
55
     */
56
    public function tableStatus(string $table, bool $fast = false)
57
    {
58
        $rows = $this->queryStatus($fast, $table);
59
        if (!($row = reset($rows))) {
60
            return null;
61
        }
62
        return $this->makeStatus($row);
63
    }
64
65
    /**
66
     * @inheritDoc
67
     */
68
    public function tableStatuses(bool $fast = false)
69
    {
70
        $tables = [];
71
        $rows = $this->queryStatus($fast);
72
        foreach ($rows as $row) {
73
            $tables[$row["Name"]] = $this->makeStatus($row);
74
        }
75
        return $tables;
76
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81
    public function tableNames()
82
    {
83
        $tables = [];
84
        $rows = $this->queryStatus(true);
85
        foreach ($rows as $row) {
86
            $tables[] = $row["Name"];
87
        }
88
        return $tables;
89
    }
90
91
    /**
92
     * @inheritDoc
93
     */
94
    public function fields(string $table)
95
    {
96
        $fields = [];
97
        foreach ($this->driver->rows("SHOW FULL COLUMNS FROM " . $this->driver->table($table)) as $row) {
98
            preg_match('~^([^( ]+)(?:\((.+)\))?( unsigned)?( zerofill)?$~', $row["Type"], $match);
99
            $matchCount = count($match);
100
            $match1 = $matchCount > 1 ? $match[1] : '';
101
            $match2 = $matchCount > 2 ? $match[2] : '';
102
            $match3 = $matchCount > 3 ? $match[3] : '';
103
            $match4 = $matchCount > 4 ? $match[4] : '';
104
105
            $field = new TableFieldEntity();
106
107
            $field->name = $row["Field"];
108
            $field->fullType = $row["Type"];
109
            $field->type = $match1;
110
            $field->length = intval($match2);
111
            $field->unsigned = ltrim($match3 . $match4);
112
            $field->default = ($row["Default"] != "" || preg_match("~char|set~", $match1) ?
113
                (preg_match('~text~', $match1) ? stripslashes(preg_replace("~^'(.*)'\$~", '\1',
114
                $row["Default"])) : $row["Default"]) : null);
115
            $field->null = ($row["Null"] == "YES");
116
            $field->autoIncrement = ($row["Extra"] == "auto_increment");
117
            $field->onUpdate = (preg_match('~^on update (.+)~i', $row["Extra"], $match) ? $match1 : ""); //! available since MySQL 5.1.23
118
            $field->collation = $row["Collation"];
119
            $field->privileges = array_flip(preg_split('~, *~', $row["Privileges"]));
120
            $field->comment = $row["Comment"];
121
            $field->primary = ($row["Key"] == "PRI");
122
            // https://mariadb.com/kb/en/library/show-columns/
123
            // https://github.com/vrana/adminer/pull/359#pullrequestreview-276677186
124
            $field->generated = preg_match('~^(VIRTUAL|PERSISTENT|STORED)~', $row["Extra"]) > 0;
125
126
            $fields[$field->name] = $field;
127
        }
128
        return $fields;
129
    }
130
131
    /**
132
     * @inheritDoc
133
     */
134
    public function supportForeignKeys(TableEntity $tableStatus)
135
    {
136
        return preg_match('~InnoDB|IBMDB2I~i', $tableStatus->engine)
137
            || (preg_match('~NDB~i', $tableStatus->engine) && $this->driver->minVersion(5.6));
138
    }
139
140
    /**
141
     * @inheritDoc
142
     */
143
    public function referencableTables(string $table)
144
    {
145
        $fields = []; // table_name => [field]
146
        foreach ($this->tableStatuses(true) as $tableName => $tableStatus) {
147
            if ($tableName === $table || !$this->supportForeignKeys($tableStatus)) {
148
                continue;
149
            }
150
            foreach ($this->fields($tableName) as $field) {
151
                if ($field->primary) {
152
                    if (!isset($fields[$tableName])) {
153
                        $fields[$tableName] = $field;
154
                    } else {
155
                        // No multi column primary key
156
                        $fields[$tableName] = null;
157
                    }
158
                }
159
            }
160
        }
161
        return array_filter($fields, function($field) {
162
            return $field !== null;
163
        });
164
    }
165
166
    /**
167
     * @inheritDoc
168
     */
169
    public function isView(TableEntity $tableStatus)
170
    {
171
        return $tableStatus->engine === null;
172
    }
173
174
    /**
175
     * @inheritDoc
176
     */
177
    public function indexes(string $table, ConnectionInterface $connection = null)
178
    {
179
        $indexes = [];
180
        foreach ($this->driver->rows("SHOW INDEX FROM " . $this->driver->table($table), $connection) as $row) {
181
            $index = new IndexEntity();
182
183
            $name = $row["Key_name"];
184
            $index->type = ($name == "PRIMARY" ? "PRIMARY" :
185
                ($row["Index_type"] == "FULLTEXT" ? "FULLTEXT" : ($row["Non_unique"] ?
186
                ($row["Index_type"] == "SPATIAL" ? "SPATIAL" : "INDEX") : "UNIQUE")));
187
            $index->columns[] = $row["Column_name"];
188
            $index->lengths[] = ($row["Index_type"] == "SPATIAL" ? null : $row["Sub_part"]);
189
            $index->descs[] = null;
190
191
            $indexes[$name] = $index;
192
        }
193
        return $indexes;
194
    }
195
196
    /**
197
     * @inheritDoc
198
     */
199
    public function foreignKeys(string $table)
200
    {
201
        static $pattern = '(?:`(?:[^`]|``)+`|"(?:[^"]|"")+")';
202
        $foreignKeys = [];
203
        $onActions = $this->driver->actions();
204
        $create_table = $this->connection->result("SHOW CREATE TABLE " . $this->driver->table($table), 1);
205
        if ($create_table) {
206
            preg_match_all("~CONSTRAINT ($pattern) FOREIGN KEY ?\\(((?:$pattern,? ?)+)\\) REFERENCES " .
207
                "($pattern)(?:\\.($pattern))? \\(((?:$pattern,? ?)+)\\)(?: ON DELETE ($onActions))" .
208
                "?(?: ON UPDATE ($onActions))?~", $create_table, $matches, PREG_SET_ORDER);
209
210
            foreach ($matches as $match) {
211
                $matchCount = count($match);
212
                $match1 = $matchCount > 1 ? $match[1] : '';
213
                $match2 = $matchCount > 2 ? $match[2] : '';
214
                $match3 = $matchCount > 3 ? $match[3] : '';
215
                $match4 = $matchCount > 4 ? $match[4] : '';
216
                $match5 = $matchCount > 5 ? $match[5] : '';
217
218
                preg_match_all("~$pattern~", $match2, $source);
219
                preg_match_all("~$pattern~", $match5, $target);
220
221
                $foreignKey = new ForeignKeyEntity();
222
223
                $foreignKey->database = $this->driver->unescapeId($match4 != "" ? $match3 : $match4);
224
                $foreignKey->table = $this->driver->unescapeId($match4 != "" ? $match4 : $match3);
225
                $foreignKey->source = array_map(function ($idf) {
226
                    return $this->driver->unescapeId($idf);
227
                }, $source[0]);
228
                $foreignKey->target = array_map(function ($idf) {
229
                    return $this->driver->unescapeId($idf);
230
                }, $target[0]);
231
                $foreignKey->onDelete = $matchCount > 6 ? $match[6] : "RESTRICT";
232
                $foreignKey->onUpdate = $matchCount > 7 ? $match[7] : "RESTRICT";
233
234
                $foreignKeys[$this->driver->unescapeId($match1)] = $foreignKey;
235
            }
236
        }
237
        return $foreignKeys;
238
    }
239
240
    /**
241
     * @inheritDoc
242
     */
243
    public function trigger(string $name, string $table = '')
244
    {
245
        if ($name == "") {
246
            return null;
247
        }
248
        $rows = $this->driver->rows("SHOW TRIGGERS WHERE `Trigger` = " . $this->driver->quote($name));
249
        if (!($row = reset($rows))) {
250
            return null;
251
        }
252
        return new TriggerEntity($row["Timing"], $row["Event"], '', '', $row["Trigger"]);
253
    }
254
255
    /**
256
     * @inheritDoc
257
     */
258
    public function triggers(string $table)
259
    {
260
        $triggers = [];
261
        foreach ($this->driver->rows("SHOW TRIGGERS LIKE " . $this->driver->quote(addcslashes($table, "%_\\"))) as $row) {
262
            $triggers[$row["Trigger"]] = new TriggerEntity($row["Timing"], $row["Event"], '', '', $row["Trigger"]);
263
        }
264
        return $triggers;
265
    }
266
267
    /**
268
     * @inheritDoc
269
     */
270
    public function triggerOptions()
271
    {
272
        return [
273
            "Timing" => ["BEFORE", "AFTER"],
274
            "Event" => ["INSERT", "UPDATE", "DELETE"],
275
            "Type" => ["FOR EACH ROW"],
276
        ];
277
    }
278
279
    /**
280
     * @inheritDoc
281
     */
282
    public function tableHelp(string $name)
283
    {
284
        $maria = preg_match('~MariaDB~', $this->connection->serverInfo());
285
        if ($this->driver->isInformationSchema($this->driver->database())) {
286
            return strtolower(($maria ? "information-schema-$name-table/" : str_replace("_", "-", $name) . "-table.html"));
287
        }
288
        if ($this->driver->database() == "mysql") {
289
            return ($maria ? "mysql$name-table/" : "system-database.html"); //! more precise link
290
        }
291
    }
292
}
293