Passed
Push — main ( 869407...009e52 )
by Thierry
01:45
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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Lagdo\DbAdmin\Driver\Sqlite\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
use function str_replace;
16
use function strtolower;
17
use function strtoupper;
18
use function preg_match;
19
use function preg_match_all;
20
use function preg_replace;
21
use function is_object;
22
use function preg_quote;
23
use function implode;
24
25
class Table extends AbstractTable
26
{
27
    /**
28
     * @inheritDoc
29
     */
30
    public function tableHelp(string $name)
31
    {
32
        if ($name == "sqlite_sequence") {
33
            return "fileformat2.html#seqtab";
34
        }
35
        if ($name == "sqlite_master") {
36
            return "fileformat2.html#$name";
37
        }
38
    }
39
40
    /**
41
     * @param string $table
42
     *
43
     * @return array
44
     */
45
    private function queryStatus(string $table = '')
46
    {
47
        $query = "SELECT name AS Name, type AS Engine, 'rowid' AS Oid, '' AS Auto_increment " .
48
            "FROM sqlite_master WHERE type IN ('table', 'view') " .
49
            ($table != "" ? "AND name = " . $this->driver->quote($table) : "ORDER BY name");
50
        return $this->driver->rows($query);
51
    }
52
53
    /**
54
     * @param array $row
55
     *
56
     * @return TableEntity
57
     */
58
    private function makeStatus(array $row)
59
    {
60
        $status = new TableEntity($row['Name']);
61
        $status->engine = $row['Engine'];
62
        $status->oid = $row['Oid'];
63
        // $status->Auto_increment = $row['Auto_increment'];
64
        $query = 'SELECT COUNT(*) FROM ' . $this->driver->escapeId($row['Name']);
65
        $status->rows = $this->connection->result($query);
66
67
        return $status;
68
    }
69
70
    /**
71
     * @inheritDoc
72
     */
73
    public function tableStatus(string $table, bool $fast = false)
74
    {
75
        $rows = $this->queryStatus($table);
76
        if (!($row = reset($rows))) {
77
            return null;
78
        }
79
        return $this->makeStatus($row);
80
    }
81
82
    /**
83
     * @inheritDoc
84
     */
85
    public function tableStatuses(bool $fast = false)
86
    {
87
        $tables = [];
88
        $rows = $this->queryStatus();
89
        foreach ($rows as $row) {
90
            $tables[$row['Name']] = $this->makeStatus($row);
91
        }
92
        return $tables;
93
    }
94
95
    /**
96
     * @inheritDoc
97
     */
98
    public function tableNames()
99
    {
100
        $tables = [];
101
        $rows = $this->queryStatus();
102
        foreach ($rows as $row) {
103
            $tables[] = $row['Name'];
104
        }
105
        return $tables;
106
    }
107
108
    /**
109
     * @inheritDoc
110
     */
111
    public function isView(TableEntity $tableStatus)
112
    {
113
        return $tableStatus->engine == 'view';
114
    }
115
116
    /**
117
     * @inheritDoc
118
     */
119
    public function supportForeignKeys(TableEntity $tableStatus)
120
    {
121
        return !$this->connection->result("SELECT sqlite_compileoption_used('OMIT_FOREIGN_KEY')");
122
    }
123
124
    /**
125
     * @param string $type
126
     *
127
     * @return string
128
     */
129
    private function rowType(string $type)
130
    {
131
        if (preg_match('~int~i', $type)) {
132
            return 'integer';
133
        }
134
        if (preg_match('~char|clob|text~i', $type)) {
135
            return 'text';
136
        }
137
        if (preg_match('~blob~i', $type)) {
138
            return 'blob';
139
        }
140
        if (preg_match('~real|floa|doub~i', $type)) {
141
            return 'real';
142
        }
143
        return 'numeric';
144
    }
145
146
    private function defaultvalue(array $row)
147
    {
148
        $default = $row["dflt_value"];
149
        if (preg_match("~'(.*)'~", $default, $match)) {
150
            return str_replace("''", "'", $match[1]);
151
        }
152
        if ($default == "NULL") {
153
            return null;
154
        }
155
        return $default;
156
    }
157
158
    /**
159
     * @param array $row
160
     *
161
     * @return TableFieldEntity
162
     */
163
    private function makeFieldEntity(array $row)
164
    {
165
        $field = new TableFieldEntity();
166
167
        $type = strtolower($row["type"]);
168
        $field->name = $row["name"];
169
        $field->type = $this->rowType($type);
170
        $field->fullType = $type;
171
        $field->default = $this->defaultvalue($row);
172
        $field->null = !$row["notnull"];
173
        $field->privileges = ["select" => 1, "insert" => 1, "update" => 1];
174
        $field->primary = $row["pk"];
175
        return $field;
176
    }
177
178
    /**
179
     * @inheritDoc
180
     */
181
    public function fields(string $table)
182
    {
183
        $fields = [];
184
        $rows = $this->driver->rows('PRAGMA table_info(' . $this->driver->table($table) . ')');
185
        $primary = "";
186
        foreach ($rows as $row) {
187
            $name = $row["name"];
188
            $type = strtolower($row["type"]);
189
            $field = $this->makeFieldEntity($row);
190
            if ($row["pk"]) {
191
                if ($primary != "") {
192
                    $fields[$primary]->autoIncrement = false;
193
                } elseif (preg_match('~^integer$~i', $type)) {
194
                    $field->autoIncrement = true;
195
                }
196
                $primary = $name;
197
            }
198
            $fields[$name] = $field;
199
        }
200
        $query = "SELECT sql FROM sqlite_master WHERE type IN ('table', 'view') AND name = " .
201
            $this->driver->quote($table);
202
        $result = $this->connection->result($query);
203
        preg_match_all('~(("[^"]*+")+|[a-z0-9_]+)\s+text\s+COLLATE\s+(\'[^\']+\'|\S+)~i',
204
            $result, $matches, PREG_SET_ORDER);
205
        foreach ($matches as $match) {
206
            $name = str_replace('""', '"', preg_replace('~^"|"$~', '', $match[1]));
207
            if (isset($fields[$name])) {
208
                $fields[$name]->collation = trim($match[3], "'");
209
            }
210
        }
211
        return $fields;
212
    }
213
214
    /**
215
     * @param string $table
216
     * @param ConnectionInterface $connection
217
     *
218
     * @return IndexEntity
219
     */
220
    private function makePrimaryIndex(string $table, ConnectionInterface $connection)
221
    {
222
        $primaryIndex = null;
223
        $query = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = " .
224
            $this->driver->quote($table);
225
        $result = $connection->result($query);
226
        if (preg_match('~\bPRIMARY\s+KEY\s*\((([^)"]+|"[^"]*"|`[^`]*`)++)~i', $result, $match)) {
227
            $primaryIndex = new IndexEntity();
228
            $primaryIndex->type = "PRIMARY";
229
            preg_match_all('~((("[^"]*+")+|(?:`[^`]*+`)+)|(\S+))(\s+(ASC|DESC))?(,\s*|$)~i',
230
                $match[1], $matches, PREG_SET_ORDER);
231
            foreach ($matches as $match) {
232
                $primaryIndex->columns[] = $this->driver->unescapeId($match[2]) . $match[4];
233
                $primaryIndex->descs[] = (preg_match('~DESC~i', $match[5]) ? '1' : null);
234
            }
235
        }
236
        if ($primaryIndex === null) {
237
            foreach ($this->fields($table) as $name => $field) {
238
                if ($field->primary) {
239
                    if (!$primaryIndex) {
240
                        $primaryIndex = new IndexEntity();
241
                    }
242
                    $primaryIndex->type = "PRIMARY";
243
                    $primaryIndex->columns = [$name];
244
                    $primaryIndex->lengths = [];
245
                    $primaryIndex->descs = [null];
246
                }
247
            }
248
        }
249
        return $primaryIndex;
250
    }
251
252
    /**
253
     * @param array $row
254
     * @param array $results
255
     * @param string $table
256
     * @param ConnectionInterface $connection
257
     *
258
     * @return IndexEntity
259
     */
260
    private function makeIndexEntity(array $row, array $results, string $table, ConnectionInterface $connection)
261
    {
262
        $index = new IndexEntity();
263
264
        $name = $row["name"];
265
        $index->type = $row["unique"] ? "UNIQUE" : "INDEX";
266
        $index->lengths = [];
267
        $index->descs = [];
268
        $columns = $this->driver->rows("PRAGMA index_info(" .
269
            $this->driver->escapeId($name) . ")", $connection);
270
        foreach ($columns as $column) {
271
            $index->columns[] = $column["name"];
272
            $index->descs[] = null;
273
        }
274
        if (preg_match('~^CREATE( UNIQUE)? INDEX ' . preg_quote($this->driver->escapeId($name) . ' ON ' .
275
            $this->driver->escapeId($table), '~') . ' \((.*)\)$~i', $results[$name], $regs)) {
276
            preg_match_all('/("[^"]*+")+( DESC)?/', $regs[2], $matches);
277
            foreach ($matches[2] as $key => $val) {
278
                if ($val) {
279
                    $index->descs[$key] = '1';
280
                }
281
            }
282
        }
283
        return $index;
284
    }
285
286
    /**
287
     * @inheritDoc
288
     */
289
    public function indexes(string $table, ConnectionInterface $connection = null)
290
    {
291
        if (!is_object($connection)) {
292
            $connection = $this->connection;
293
        }
294
        $primaryIndex = $this->makePrimaryIndex($table, $connection);
295
        if ($primaryIndex === null) {
296
            return [];
297
        }
298
299
        $indexes = [];
300
        $query = "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = " .
301
            $this->driver->quote($table);
302
        $results = $this->driver->keyValues($query, $connection);
303
        $rows = $this->driver->rows("PRAGMA index_list(" .
304
            $this->driver->table($table) . ")", $connection);
305
        foreach ($rows as $row) {
306
            $index = $this->makeIndexEntity($row, $results, $table, $connection);
307
            if ($index->type === 'UNIQUE' && $index->columns == $primaryIndex->columns &&
308
                $index->descs == $primaryIndex->descs && preg_match("~^sqlite_~", $name)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $name seems to be never defined.
Loading history...
309
                $indexes[$name] = $index;
310
            }
311
        }
312
313
        return $indexes;
314
    }
315
316
    /**
317
     * @inheritDoc
318
     */
319
    public function foreignKeys(string $table)
320
    {
321
        $foreignKeys = [];
322
        foreach ($this->driver->rows("PRAGMA foreign_key_list(" . $this->driver->table($table) . ")") as $row) {
323
            $name = $row["id"];
324
            if (!isset($foreignKeys[$name])) {
325
                $foreignKeys[$name] = new ForeignKeyEntity();
326
            }
327
            //! idf_unescape in SQLite2
328
            $foreignKeys[$name]->source[] = $row["from"];
329
            $foreignKeys[$name]->target[] = $row["to"];
330
        }
331
        return $foreignKeys;
332
    }
333
334
    /**
335
     * @inheritDoc
336
     */
337
    public function trigger(string $name, string $table = '')
338
    {
339
        if ($name == "") {
340
            return new TriggerEntity('', '', "BEGIN\n\t;\nEND");
341
        }
342
        $idf = '(?:[^`"\s]+|`[^`]*`|"[^"]*")+';
343
        $options = $this->triggerOptions();
344
        preg_match("~^CREATE\\s+TRIGGER\\s*$idf\\s*(" . implode("|", $options["Timing"]) .
345
            ")\\s+([a-z]+)(?:\\s+OF\\s+($idf))?\\s+ON\\s*$idf\\s*(?:FOR\\s+EACH\\s+ROW\\s)?(.*)~is",
346
            $this->connection->result("SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = " .
347
            $this->driver->quote($name)), $match);
348
        $of = $match[3];
349
        return new TriggerEntity(strtoupper($match[1]), strtoupper($match[2]), $match[4],
350
            ($of[0] == '`' || $of[0] == '"' ? $this->driver->unescapeId($of) : $of), $name);
351
    }
352
353
    /**
354
     * @inheritDoc
355
     */
356
    public function triggers(string $table)
357
    {
358
        $triggers = [];
359
        $options = $this->triggerOptions();
360
        $query = "SELECT * FROM sqlite_master WHERE type = 'trigger' AND tbl_name = " . $this->driver->quote($table);
361
        foreach ($this->driver->rows($query) as $row) {
362
            preg_match('~^CREATE\s+TRIGGER\s*(?:[^`"\s]+|`[^`]*`|"[^"]*")+\s*(' .
363
                implode("|", $options["Timing"]) . ')\s*(.*?)\s+ON\b~i', $row["sql"], $match);
364
            $triggers[$row["name"]] = new TriggerEntity($match[1], $match[2], '', '', $row["name"]);
365
        }
366
        return $triggers;
367
    }
368
369
    /**
370
     * @inheritDoc
371
     */
372
    public function triggerOptions()
373
    {
374
        return [
375
            "Timing" => ["BEFORE", "AFTER", "INSTEAD OF"],
376
            "Event" => ["INSERT", "UPDATE", "UPDATE OF", "DELETE"],
377
            "Type" => ["FOR EACH ROW"],
378
        ];
379
    }
380
}
381