Passed
Push — main ( e2fa8b...75aa7b )
by Thierry
39:25 queued 27:26
created

TableDumpTrait::convertToString()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 6
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 12
rs 9.2222
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Facades\Traits;
4
5
use Lagdo\DbAdmin\Driver\Db\StatementInterface;
6
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
7
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
8
9
use function array_filter;
10
use function array_keys;
11
use function array_map;
12
use function count;
13
use function implode;
14
use function is_numeric;
15
use function preg_match;
16
use function str_replace;
17
18
trait TableDumpTrait
19
{
20
    /**
21
     * The queries generated by the dump
22
     *
23
     * @var array
24
     */
25
    private $queries = [];
26
27
    /**
28
     * The dump options
29
     *
30
     * @var array
31
     */
32
    private $options;
33
34
    /**
35
     * Print CSV row
36
     *
37
     * @param array  $row
38
     *
39
     * @return void
40
     */
41
    private function dumpCsv(array $row)
42
    {
43
        // From functions.inc.php
44
        foreach ($row as $key => $val) {
45
            if (preg_match('~["\n,;\t]|^0|\.\d*0$~', $val ?? '') || $val === '') {
46
                $row[$key] = '"' . str_replace('"', '""', $val) . '"';
47
            }
48
        }
49
        $separator = match($this->options['format']) {
50
            'csv' => ',',
51
            'tsv' => "\t",
52
            default => ';',
53
        };
54
        $this->queries[] = implode($separator, $row);
55
    }
56
57
    /**
58
     * Convert a value to string
59
     *
60
     * @param mixed  $value
61
     * @param TableFieldEntity $field
62
     *
63
     * @return string
64
     */
65
    private function convertToString($value, TableFieldEntity $field): string
66
    {
67
        // From functions.inc.php
68
        if ($value === null) {
69
            return 'NULL';
70
        }
71
72
        if (!preg_match($this->driver->numberRegex(), $field->type) ||
73
            preg_match('~\[~', $field->fullType) && is_numeric($value)) {
74
            $value = $this->driver->quote(($value === false ? 0 : $value));
75
        }
76
        return $this->driver->unconvertField($field, $value);
77
    }
78
79
    /**
80
     * @param string $table
81
     *
82
     * @return void
83
     */
84
    private function dumpTruncateQuery(string $table)
85
    {
86
        if ($this->options['format'] === 'sql' &&
87
            $this->options['data_style'] === 'TRUNCATE+INSERT') {
88
            $this->queries[] = $this->driver->getTruncateTableQuery($table) . ";\n";
89
        }
90
    }
91
92
    /**
93
     * @param DataDump $dump
94
     * @param array $row
95
     * @param StatementInterface $statement
96
     *
97
     * @return array
98
     */
99
    private function getDataRowKeys(DataDump $dump, array $row, StatementInterface $statement): array
100
    {
101
        $values = [];
102
        $keys = [];
103
        // For is preferred to foreach because the values are not used.
104
        // foreach ($row as $val) {
105
        // }
106
        $rowCount = count($row);
107
        for ($i = 0; $i < $rowCount; $i++) {
108
            $field = $statement->fetchField();
109
            $keys[] = $field->name();
110
            $key = $this->driver->escapeId($field->name());
111
            $values[] = "$key = VALUES($key)";
112
        }
113
        $dump->suffix = $this->options['data_style'] !== 'INSERT+UPDATE' ? ';' :
114
            "\nON DUPLICATE KEY UPDATE " . implode(', ', $values) . ';';
115
116
        return $keys;
117
    }
118
119
    /**
120
     * @param DataDump $dump
121
     * @param array $fields
122
     * @param array $row
123
     * @param array $keys
124
     *
125
     * @return void
126
     */
127
    private function dumpRow(DataDump $dump, array $fields, array $row, array $keys)
128
    {
129
        if ($this->options['format'] !== 'sql') {
130
            if ($this->options['data_style'] === 'table') {
131
                $this->dumpCsv($keys);
132
                $this->options['data_style'] = 'INSERT';
133
            }
134
            $this->dumpCsv($row);
135
            return;
136
        }
137
138
        if ($dump->insert === '') {
139
            $dump->insert = 'INSERT INTO ' . $this->driver->escapeTableName($dump->table) . ' (' .
140
                implode(', ', array_map(function ($key) {
141
                    return $this->driver->escapeId($key);
142
                }, $keys)) . ') VALUES';
143
        }
144
        foreach ($row as $key => $val) {
145
            $field = $fields[$key];
146
            $row[$key] = $this->convertToString($val, $field);
147
        }
148
149
        $dump->addRow($row);
150
        if ($dump->limitExceeded()) {
151
            $this->queries[] = $dump->makeQuery();
152
        }
153
    }
154
155
    /**
156
     * @param DataDump $dump
157
     * @param StatementInterface $statement
158
     *
159
     * @return void
160
     */
161
    private function dumpRows(DataDump $dump, StatementInterface $statement)
162
    {
163
        $fields = $this->options['format'] !== 'sql' ? [] : $this->driver->fields($dump->table);
164
        $keys = [];
165
        $fetchFunction = $dump->table !== '' ?
166
            fn($statement) => $statement->fetchAssoc() :
167
            fn($statement) => $statement->fetchRow();
168
        while ($row = $fetchFunction($statement)) {
169
            if (empty($keys)) {
170
                $keys = $this->getDataRowKeys($dump, $row, $statement);
171
            }
172
            $this->dumpRow($dump, $fields, $row, $keys);
173
        }
174
        if (count($dump->dataRows) > 0) {
175
            $this->queries[] = $dump->makeQuery();
176
        }
177
    }
178
179
    /** Export table data
180
     *
181
     * @param string $table
182
     *
183
     * @return void
184
     */
185
    private function dumpTableData(string $table)
186
    {
187
        if (!$this->options['data_style']) {
188
            return;
189
        }
190
        $fields = $this->driver->fields($table);
191
        $query = 'SELECT *' . $this->driver->convertFields($fields, $fields) .
192
            ' FROM ' . $this->driver->escapeTableName($table);
193
        // 1 - MYSQLI_USE_RESULT //! enum and set as numbers
194
        $statement = $this->driver->execute($query);
195
        if (!$statement) {
196
            if ($this->options['format'] === 'sql') {
197
                $this->queries[] = '-- ' . str_replace("\n", ' ', $this->driver->error()) . "\n";
198
            }
199
            return;
200
        }
201
202
        $maxRowSize = ($this->driver->jush() === 'sqlite' ? 0 : 1048576); // default, minimum is 1024
203
        $separator = $maxRowSize > 0 ? "\n" : ' ';
204
        $dump = new DataDump($table, $maxRowSize, $separator);
205
206
        $this->dumpTruncateQuery($table);
207
        $this->dumpRows($dump, $statement);
208
    }
209
210
    /**
211
     * @param string $table
212
     * @param string $style
213
     * @param int $tableType
214
     *
215
     * @return string
216
     */
217
    private function getCreateQuery(string $table, string $style, int $tableType): string
218
    {
219
        if ($tableType !== 2) {
220
            return $this->driver->getCreateTableQuery($table,
221
                $this->options['autoIncrement'], $style);
222
        }
223
224
        $fields = [];
225
        foreach ($this->driver->fields($table) as $name => $field) {
226
            $fields[] = $this->driver->escapeId($name) . ' ' . $field->fullType;
227
        }
228
        return 'CREATE TABLE ' . $this->driver->escapeTableName($table) .
229
            ' (' . implode(', ', $fields) . ')';
230
    }
231
232
    /**
233
     * Export table structure
234
     *
235
     * @param string $table
236
     * @param string $style
237
     * @param int    $tableType       0 table, 1 view, 2 temporary view table
238
     *
239
     * @return void
240
     */
241
    private function addCreateQuery(string $table, string $style, int $tableType): void
242
    {
243
        $query = $this->getCreateQuery($table, $style, $tableType);
244
        if (!$query) {
245
            return;
246
        }
247
248
        $this->driver->setUtf8mb4($query);
249
        if ($style === 'DROP+CREATE' || $tableType === 1) {
250
            $this->queries[] = 'DROP ' . ($tableType === 2 ? 'VIEW' : 'TABLE') .
251
                ' IF EXISTS ' . $this->driver->escapeTableName($table) . ';';
252
        }
253
        if ($tableType === 1) {
254
            $query = $this->driver->removeDefiner($query);
255
        }
256
        $this->queries[] = "$query;\n";
257
    }
258
259
    /**
260
     * Export table structure
261
     *
262
     * @param string $table
263
     * @param string $style
264
     * @param int    $tableType       0 table, 1 view, 2 temporary view table
265
     *
266
     * @return void
267
     */
268
    private function dumpCreateTableOrView(string $table, string $style, int $tableType = 0): void
269
    {
270
        // From adminer.inc.php
271
        if ($this->options['format'] !== 'sql') {
272
            $this->queries[] = "\xef\xbb\xbf"; // UTF-8 byte order mark
273
            if ($style) {
274
                $this->dumpCsv(array_keys($this->driver->fields($table)));
275
            }
276
            return;
277
        }
278
        if (!$style) {
279
            return;
280
        }
281
282
        $this->addCreateQuery($table, $style, $tableType);
283
    }
284
285
    /**
286
     * @param string $table
287
     *
288
     * @return void
289
     */
290
    private function dumpTableTriggers(string $table): void
291
    {
292
        if (($triggers = $this->driver->getCreateTriggerQuery($table)) !== '') {
293
            $this->queries[] = '';
294
            $this->queries[] = 'DELIMITER ;;';
295
            $this->queries[] = $triggers;
296
            $this->queries[] = 'DELIMITER ;';
297
        }
298
    }
299
300
    /**
301
     * @param TableEntity $tableStatus
302
     * @param bool $dumpTable
303
     * @param bool $dumpData
304
     *
305
     * @return void
306
     */
307
    private function dumpTable(TableEntity $tableStatus, bool $dumpTable, bool $dumpData): void
308
    {
309
        $style = $dumpTable ? $this->options['table_style'] : '';
310
        $tableType = $this->driver->isView($tableStatus) ? 2 : 1;
311
        $this->dumpCreateTableOrView($tableStatus->name, $style, $tableType);
312
        if ($dumpData) {
313
            $this->dumpTableData($tableStatus->name);
314
        }
315
        if ($this->options['to_sql'] && $this->options['triggers'] && $dumpTable) {
316
            $this->dumpTableTriggers($tableStatus->name);
317
        }
318
    }
319
320
    /**
321
     * @param array<TableEntity> $tableStatuses
322
     * @param array $tableOptions
323
     *
324
     * @return void
325
     */
326
    private function dumpTables(array $tableStatuses, array $tableOptions): void
327
    {
328
        foreach ($tableStatuses as $status) {
329
            $this->queries[] = '';
330
            $options = $tableOptions[$status->name] ?? $tableOptions['*'];
331
            $this->dumpTable($status, $options['table'], $options['data']);
332
        }
333
        if ($this->driver->jush() !== 'pgsql') {
334
            return;
335
        }
336
337
        // Add FKs after creating tables (except in MySQL which uses SET FOREIGN_KEY_CHECKS=0)
338
        $this->queries[] = '';
339
        $tables = array_filter($tableStatuses,
340
            fn($status) => !$this->driver->isView($status));
341
        foreach ($tables as $status) {
342
            $queries = $this->driver->getForeignKeysQueries($status);
343
            foreach ($queries as $query) {
344
                $this->queries[] = $query;
345
            }
346
            if (count($queries) > 0) {
347
                $this->queries[] = '';
348
            }
349
        }
350
    }
351
352
    /**
353
     * @param array $tableStatuses
354
     *
355
     * @return void
356
     */
357
    private function dumpViews(array $tableStatuses)
358
    {
359
        $views = array_filter($tableStatuses,
360
            fn($status) => $this->driver->isView($status));
361
        foreach ($views as $view) {
362
            $this->dumpCreateTableOrView($view->name, $this->options['table_style'], 1);
363
        }
364
    }
365
}
366