ExportFacade::dumpTruncateQuery()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Driver\Facades;
4
5
use Lagdo\DbAdmin\Db\Page\DataDump;
6
use Lagdo\DbAdmin\Db\Page\TableExport;
7
use Lagdo\DbAdmin\Driver\Db\StatementInterface;
8
use Lagdo\DbAdmin\Driver\Entity\FieldType;
9
use Lagdo\DbAdmin\Driver\Entity\RoutineEntity;
10
use Lagdo\DbAdmin\Driver\Entity\RoutineInfoEntity;
11
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
12
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
13
use Exception;
14
15
use function array_filter;
16
use function array_keys;
17
use function array_map;
18
use function count;
19
use function implode;
20
use function is_numeric;
21
use function ksort;
22
use function preg_match;
23
use function rtrim;
24
use function str_replace;
25
use function trim;
26
27
/**
28
 * Facade to export functions
29
 */
30
class ExportFacade extends AbstractFacade
31
{
32
    /**
33
     * The databases to dump
34
     *
35
     * @var array
36
     */
37
    private $databases;
0 ignored issues
show
introduced by
The private property $databases is not used, and could be removed.
Loading history...
38
39
    /**
40
     * The tables to dump
41
     *
42
     * @var array
43
     */
44
    private $tables;
0 ignored issues
show
introduced by
The private property $tables is not used, and could be removed.
Loading history...
45
46
    /**
47
     * The queries generated by the dump
48
     *
49
     * @var array
50
     */
51
    private $queries = [];
52
53
    /**
54
     * The dump options
55
     *
56
     * @var array
57
     */
58
    private $options;
59
60
    /**
61
     * @return TableExport
62
     */
63
    private function export(): TableExport
64
    {
65
        return new TableExport($this->page, $this->driver, $this->utils);
0 ignored issues
show
Bug introduced by
It seems like $this->driver can also be of type null; however, parameter $driver of Lagdo\DbAdmin\Db\Page\TableExport::__construct() does only seem to accept Lagdo\DbAdmin\Driver\DriverInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

65
        return new TableExport($this->page, /** @scrutinizer ignore-type */ $this->driver, $this->utils);
Loading history...
66
    }
67
68
    /**
69
     * Print CSV row
70
     *
71
     * @param array  $row
72
     *
73
     * @return void
74
     */
75
    private function dumpCsv(array $row)
76
    {
77
        // From functions.inc.php
78
        foreach ($row as $key => $val) {
79
            if (preg_match('~["\n,;\t]|^0|\.\d*0$~', $val ?? '') || $val === '') {
80
                $row[$key] = '"' . str_replace('"', '""', $val) . '"';
81
            }
82
        }
83
        $separator = match($this->options['format']) {
84
            'csv' => ',',
85
            'tsv' => "\t",
86
            default => ';',
87
        };
88
        $this->queries[] = implode($separator, $row);
89
    }
90
91
    /**
92
     * Convert a value to string
93
     *
94
     * @param mixed  $value
95
     * @param TableFieldEntity $field
96
     *
97
     * @return string
98
     */
99
    private function convertToString($value, TableFieldEntity $field): string
100
    {
101
        // From functions.inc.php
102
        if ($value === null) {
103
            return 'NULL';
104
        }
105
106
        if (!preg_match($this->driver->numberRegex(), $field->type) ||
0 ignored issues
show
Bug introduced by
The method numberRegex() does not exist on null. ( Ignorable by Annotation )

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

106
        if (!preg_match($this->driver->/** @scrutinizer ignore-call */ numberRegex(), $field->type) ||

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...
107
            preg_match('~\[~', $field->fullType) && is_numeric($value)) {
108
            $value = $this->driver->quote(($value === false ? 0 : $value));
109
        }
110
        return $this->driver->unconvertField($field, $value);
111
    }
112
113
    /**
114
     * @param string $table
115
     *
116
     * @return void
117
     */
118
    private function dumpTruncateQuery(string $table)
119
    {
120
        if ($this->options['format'] === 'sql' &&
121
            $this->options['data_style'] === 'TRUNCATE+INSERT') {
122
            $this->queries[] = $this->driver->getTruncateTableQuery($table) . ";\n";
123
        }
124
    }
125
126
    /**
127
     * @param DataDump $dump
128
     * @param array $row
129
     * @param StatementInterface $statement
130
     *
131
     * @return array
132
     */
133
    private function getDataRowKeys(DataDump $dump, array $row, StatementInterface $statement): array
134
    {
135
        $values = [];
136
        $keys = [];
137
        // For is preferred to foreach because the values are not used.
138
        // foreach ($row as $val) {
139
        // }
140
        $rowCount = count($row);
141
        for ($i = 0; $i < $rowCount; $i++) {
142
            $field = $statement->fetchField();
143
            $keys[] = $field->name();
144
            $key = $this->driver->escapeId($field->name());
145
            $values[] = "$key = VALUES($key)";
146
        }
147
        $dump->suffix = $this->options['data_style'] !== 'INSERT+UPDATE' ? ';' :
148
            "\nON DUPLICATE KEY UPDATE " . implode(', ', $values) . ';';
149
150
        return $keys;
151
    }
152
153
    /**
154
     * @param DataDump $dump
155
     * @param array $fields
156
     * @param array $row
157
     * @param array $keys
158
     *
159
     * @return void
160
     */
161
    private function dumpRow(DataDump $dump, array $fields, array $row, array $keys)
162
    {
163
        if ($this->options['format'] !== 'sql') {
164
            if ($this->options['data_style'] === 'table') {
165
                $this->dumpCsv($keys);
166
                $this->options['data_style'] = 'INSERT';
167
            }
168
            $this->dumpCsv($row);
169
            return;
170
        }
171
172
        if ($dump->insert === '') {
173
            $dump->insert = 'INSERT INTO ' . $this->driver->escapeTableName($dump->table) . ' (' .
174
                implode(', ', array_map(function ($key) {
175
                    return $this->driver->escapeId($key);
176
                }, $keys)) . ') VALUES';
177
        }
178
        foreach ($row as $key => $val) {
179
            $field = $fields[$key];
180
            $row[$key] = $this->convertToString($val, $field);
181
        }
182
183
        $dump->addRow($row);
184
        if ($dump->limitExceeded()) {
185
            $this->queries[] = $dump->makeQuery();
186
        }
187
    }
188
189
    /**
190
     * @param DataDump $dump
191
     * @param StatementInterface $statement
192
     *
193
     * @return void
194
     */
195
    private function dumpRows(DataDump $dump, StatementInterface $statement)
196
    {
197
        $fields = $this->options['format'] !== 'sql' ? [] : $this->driver->fields($dump->table);
198
        $keys = [];
199
        $fetchFunction = $dump->table !== '' ?
200
            fn($statement) => $statement->fetchAssoc() :
201
            fn($statement) => $statement->fetchRow();
202
        while ($row = $fetchFunction($statement)) {
203
            if (empty($keys)) {
204
                $keys = $this->getDataRowKeys($dump, $row, $statement);
205
            }
206
            $this->dumpRow($dump, $fields, $row, $keys);
207
        }
208
        if (count($dump->dataRows) > 0) {
209
            $this->queries[] = $dump->makeQuery();
210
        }
211
    }
212
213
    /** Export table data
214
     *
215
     * @param string $table
216
     *
217
     * @return void
218
     */
219
    private function dumpTableData(string $table)
220
    {
221
        if (!$this->options['data_style']) {
222
            return;
223
        }
224
        $fields = $this->driver->fields($table);
225
        $query = 'SELECT *' . $this->driver->convertFields($fields, $fields) .
226
            ' FROM ' . $this->driver->escapeTableName($table);
227
        // 1 - MYSQLI_USE_RESULT //! enum and set as numbers
228
        $statement = $this->driver->execute($query);
229
        if (!$statement) {
230
            if ($this->options['format'] === 'sql') {
231
                $this->queries[] = '-- ' . str_replace("\n", ' ', $this->driver->error()) . "\n";
232
            }
233
            return;
234
        }
235
236
        $maxRowSize = ($this->driver->jush() === 'sqlite' ? 0 : 1048576); // default, minimum is 1024
237
        $separator = $maxRowSize > 0 ? "\n" : ' ';
238
        $dump = new DataDump($table, $maxRowSize, $separator);
239
240
        $this->dumpTruncateQuery($table);
241
        $this->dumpRows($dump, $statement);
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type true; however, parameter $statement of Lagdo\DbAdmin\Db\Driver\...xportFacade::dumpRows() does only seem to accept Lagdo\DbAdmin\Driver\Db\StatementInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

241
        $this->dumpRows($dump, /** @scrutinizer ignore-type */ $statement);
Loading history...
242
    }
243
244
    /**
245
     * @param string $table
246
     * @param string $style
247
     * @param int $tableType
248
     *
249
     * @return string
250
     */
251
    private function getCreateQuery(string $table, string $style, int $tableType): string
252
    {
253
        if ($tableType !== 2) {
254
            return $this->driver->getCreateTableQuery($table,
255
                $this->options['autoIncrement'], $style);
256
        }
257
258
        $fields = [];
259
        foreach ($this->driver->fields($table) as $name => $field) {
260
            $fields[] = $this->driver->escapeId($name) . ' ' . $field->fullType;
261
        }
262
        return 'CREATE TABLE ' . $this->driver->escapeTableName($table) .
263
            ' (' . implode(', ', $fields) . ')';
264
    }
265
266
    /**
267
     * Export table structure
268
     *
269
     * @param string $table
270
     * @param string $style
271
     * @param int    $tableType       0 table, 1 view, 2 temporary view table
272
     *
273
     * @return void
274
     */
275
    private function addCreateQuery(string $table, string $style, int $tableType): void
276
    {
277
        $query = $this->getCreateQuery($table, $style, $tableType);
278
        if (!$query) {
279
            return;
280
        }
281
282
        $this->driver->setUtf8mb4($query);
283
        if ($style === 'DROP+CREATE' || $tableType === 1) {
284
            $this->queries[] = 'DROP ' . ($tableType === 2 ? 'VIEW' : 'TABLE') .
285
                ' IF EXISTS ' . $this->driver->escapeTableName($table) . ';';
286
        }
287
        if ($tableType === 1) {
288
            $query = $this->driver->removeDefiner($query);
289
        }
290
        $this->queries[] = "$query;\n";
291
    }
292
293
    /**
294
     * Export table structure
295
     *
296
     * @param string $table
297
     * @param string $style
298
     * @param int    $tableType       0 table, 1 view, 2 temporary view table
299
     *
300
     * @return void
301
     */
302
    private function dumpCreateTableOrView(string $table, string $style, int $tableType = 0): void
303
    {
304
        // From adminer.inc.php
305
        if ($this->options['format'] !== 'sql') {
306
            $this->queries[] = "\xef\xbb\xbf"; // UTF-8 byte order mark
307
            if ($style) {
308
                $this->dumpCsv(array_keys($this->driver->fields($table)));
309
            }
310
            return;
311
        }
312
        if (!$style) {
313
            return;
314
        }
315
316
        $this->addCreateQuery($table, $style, $tableType);
317
    }
318
319
    /**
320
     * @param string $table
321
     *
322
     * @return void
323
     */
324
    private function dumpTableTriggers(string $table): void
325
    {
326
        if (($triggers = $this->driver->getCreateTriggerQuery($table)) !== '') {
327
            $this->queries[] = '';
328
            $this->queries[] = 'DELIMITER ;;';
329
            $this->queries[] = $triggers;
330
            $this->queries[] = 'DELIMITER ;';
331
        }
332
    }
333
334
    /**
335
     * @param TableEntity $tableStatus
336
     * @param bool $dumpTable
337
     * @param bool $dumpData
338
     *
339
     * @return void
340
     */
341
    private function dumpTable(TableEntity $tableStatus, bool $dumpTable, bool $dumpData): void
342
    {
343
        $style = $dumpTable ? $this->options['table_style'] : '';
344
        $tableType = $this->driver->isView($tableStatus) ? 2 : 1;
345
        $this->dumpCreateTableOrView($tableStatus->name, $style, $tableType);
346
        if ($dumpData) {
347
            $this->dumpTableData($tableStatus->name);
348
        }
349
        if ($this->options['to_sql'] && $this->options['triggers'] && $dumpTable) {
350
            $this->dumpTableTriggers($tableStatus->name);
351
        }
352
    }
353
354
    /**
355
     * @param array<TableEntity> $tableStatuses
356
     * @param array $tableOptions
357
     *
358
     * @return void
359
     */
360
    private function dumpTables(array $tableStatuses, array $tableOptions): void
361
    {
362
        foreach ($tableStatuses as $status) {
363
            $this->queries[] = '';
364
            $options = $tableOptions[$status->name] ?? $tableOptions['*'];
365
            $this->dumpTable($status, $options['table'], $options['data']);
366
        }
367
        if ($this->driver->jush() !== 'pgsql') {
368
            return;
369
        }
370
371
        // Add FKs after creating tables (except in MySQL which uses SET FOREIGN_KEY_CHECKS=0)
372
        $this->queries[] = '';
373
        $tables = array_filter($tableStatuses,
374
            fn($status) => !$this->driver->isView($status));
375
        foreach ($tables as $status) {
376
            $queries = $this->driver->getForeignKeysQueries($status);
377
            foreach ($queries as $query) {
378
                $this->queries[] = $query;
379
            }
380
            if (count($queries) > 0) {
381
                $this->queries[] = '';
382
            }
383
        }
384
    }
385
386
    /**
387
     * @param array $tableStatuses
388
     *
389
     * @return void
390
     */
391
    private function dumpViews(array $tableStatuses)
392
    {
393
        $views = array_filter($tableStatuses,
394
            fn($status) => $this->driver->isView($status));
395
        foreach ($views as $view) {
396
            $this->dumpCreateTableOrView($view->name, $this->options['table_style'], 1);
397
        }
398
    }
399
400
    /**
401
     * Get data for export
402
     *
403
     * @param string $database      The database name
404
     * @param string $table
405
     *
406
     * @return array
407
     */
408
    public function getExportOptions(string $database, string $table = ''): array
409
    {
410
        $export = $this->export();
411
        return $database === '' ? [
412
            'databases' => $export->getDatabases(),
413
            'options' => $export->getBaseOptions($database, $table),
414
            'prefixes' => [],
415
        ] : [
416
            'tables' => $export->getDbTables(),
417
            'options' => $export->getBaseOptions($database, $table),
418
            'prefixes' => [],
419
        ];
420
    }
421
422
    /**
423
     * @return array
424
     */
425
    public function getSelectValues(): array
426
    {
427
        $export = $this->export();
428
        return [
429
            'output' => array_keys($export->getSelectOutputValues()),
430
            'format' => array_keys($export->getSelectFormatValues()),
431
            'db_style' => $export->getSelectDatabaseValues(),
432
            'table_style' => $export->getSelectTableValues(),
433
            'data_style' => $export->getSelectDataValues(),
434
        ];
435
    }
436
437
    /**
438
     * @param array<FieldType> $params
439
     *
440
     * @return string
441
     */
442
    private function getRoutineParams(array $params): string
443
    {
444
        // From dump.inc.php create_routine()
445
        $params = array_filter($params, fn($param) => $param->name !== '');
446
        ksort($params); // enforce params order
447
        $regex = "~^(" . $this->driver->inout() . ")\$~";
448
449
        $params = array_map(function($param) use($regex) {
450
            $inout = preg_match($regex, $param->inout) ? "{$param->inout} " : '';
451
            return $inout . $this->driver->escapeId($param->name) .
452
                $this->driver->processType($param, 'CHARACTER SET');
453
        },$params);
454
        return implode(', ', $params);
455
    }
456
457
    /**
458
     * Generate SQL query for creating routine
459
     *
460
     * @param RoutineEntity $routine
461
     * @param RoutineInfoEntity $routineInfo
462
     *
463
     * @return string
464
     */
465
    private function getRoutineQuery(RoutineEntity $routine, RoutineInfoEntity $routineInfo): string
466
    {
467
        // From dump.inc.php create_routine()
468
        $routineName = $this->driver->escapeId(trim($routine->name));
469
        $routineParams = $this->getRoutineParams($routineInfo->params);
470
        $routineReturns = $routine->type !== 'FUNCTION' ? '' :
471
            ' RETURNS' . $this->driver->processType($routineInfo->return, 'CHARACTER SET');
0 ignored issues
show
Bug introduced by
It seems like $routineInfo->return can also be of type null; however, parameter $field of Lagdo\DbAdmin\Driver\AbstractDriver::processType() does only seem to accept Lagdo\DbAdmin\Driver\Entity\FieldType, maybe add an additional type check? ( Ignorable by Annotation )

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

471
            ' RETURNS' . $this->driver->processType(/** @scrutinizer ignore-type */ $routineInfo->return, 'CHARACTER SET');
Loading history...
472
        $routineLanguage = $routineInfo->language ? " LANGUAGE {$routineInfo->language}" : '';
473
        $definition = rtrim($routineInfo->definition, ';');
474
        $routineDefinition = $this->driver->jush() !== 'pgsql' ? "\n$definition;" :
475
            ' AS ' . $this->driver->quote($definition);
476
477
        return "CREATE {$routine->type} $routineName ($routineParams)" .
478
            "{$routineReturns}{$routineLanguage}{$routineDefinition};";
479
    }
480
481
    /**
482
     * Dump types in the connected database
483
     *
484
     * @return void
485
     */
486
    private function dumpTypes(): void
487
    {
488
        if (!$this->options['types']) {
489
            return;
490
        }
491
492
        // From dump.inc.php
493
        $style = $this->options['db_style'];
494
        foreach ($this->driver->userTypes(true) as $type) {
495
            $this->queries[] = ''; // Empty line
496
            if (count($type->enums) === 0) {
497
                //! https://github.com/postgres/postgres/blob/REL_17_4/src/bin/pg_dump/pg_dump.c#L10846
498
                $this->queries[] = "-- Could not export type {$type->name}";
499
                continue;
500
            }
501
502
            $typeName = $this->driver->escapeId($type->name);
503
            if ($style !== 'DROP+CREATE') {
504
                $this->queries[] = "DROP TYPE IF EXISTS $typeName;;";
505
            }
506
            $enums = implode("', '", $type->enums);
507
            $this->queries[] = "CREATE TYPE $typeName AS ENUM ('$enums');";
508
        }
509
    }
510
511
    /**
512
     * Dump routines in the connected database
513
     *
514
     * @return void
515
     */
516
    private function dumpRoutines(): void
517
    {
518
        if (!$this->options['routines']) {
519
            return;
520
        }
521
522
        // From dump.inc.php
523
        $style = $this->options['db_style'];
524
        foreach ($this->driver->routines() as $routine) {
525
            $routineName = $this->driver->escapeId(trim($routine->name));
526
            $routineInfo = $this->driver->routine($routine->specificName, $routine->type);
527
            if ($routineInfo === null) {
528
                continue;
529
            }
530
531
            $create = $this->getRoutineQuery($routine, $routineInfo);
532
            $this->driver->setUtf8mb4($create);
533
            $this->queries[] = ''; // Empty line
534
            if ($style !== 'DROP+CREATE') {
535
                $this->queries[] = "DROP {$routine->type} IF EXISTS $routineName;;";
536
            }
537
            $this->queries[] = $create;
538
        }
539
    }
540
541
    /**
542
     * Dump events in the connected database
543
     *
544
     * @return void
545
     */
546
    private function dumpEvents(): void
547
    {
548
        if (!$this->options['events']) {
549
            return;
550
        }
551
552
        // From dump.inc.php
553
        $style = $this->options['db_style'];
554
        foreach ($this->driver->rows('SHOW EVENTS') as $row) {
555
            $sql = 'SHOW CREATE EVENT ' . $this->driver->escapeId($row['Name']);
556
            $create = $this->driver->removeDefiner($this->driver->result($sql, 3));
557
            $this->driver->setUtf8mb4($create);
558
            $this->queries[] = ''; // Empty line
559
            if ($style !== 'DROP+CREATE') {
560
                $this->queries[] = 'DROP EVENT IF EXISTS ' . $this->driver->escapeId($row['Name']) . ';;';
561
            }
562
            $this->queries[] = "$create;;\n";
563
        }
564
    }
565
566
    /**
567
     * @param string $database
568
     *
569
     * @return void
570
     */
571
    private function dumpUseDatabaseQuery(string $database): void
572
    {
573
        $style = $this->options['db_style'];
574
        if ($style === '' || !preg_match('~sql~', $this->options['format'])) {
575
            return;
576
        }
577
578
        $this->queries[] = $this->driver->getUseDatabaseQuery($database, $style);
579
    }
580
581
    /**
582
     * @param string $database
583
     * @param array $tableOptions
584
     *
585
     * @return void
586
     */
587
    private function dumpDatabase(string $database, array $tableOptions): void
588
    {
589
        $this->driver->openConnection($database); // New connection
590
        $this->dumpUseDatabaseQuery($database);
591
592
        if ($this->options['to_sql']) {
593
            $this->dumpTypes();
594
            $this->dumpRoutines();
595
            $this->dumpEvents();
596
        }
597
598
        if (!$this->options['table_style'] && !$this->options['data_style']) {
599
            return;
600
        }
601
602
        $statuses = array_filter($this->driver->tableStatuses(true), fn($status) =>
603
            isset($tableOptions['*']) || isset($tableOptions[$status->name]));
604
        $this->dumpTables($statuses, $tableOptions);
605
        // Dump the views after all the tables
606
        $this->dumpViews($statuses);
607
    }
608
609
    /**
610
     * @return array
611
     */
612
    private function getDatabaseExportHeaders(): array
613
    {
614
        $headers = [
615
            'version' => $this->driver->version(),
616
            'driver' => $this->driver->name(),
617
            'server' => str_replace("\n", ' ', $this->driver->serverInfo()),
618
            'sql' => false,
619
            'data_style' => false,
620
        ];
621
        if ($this->driver->jush() === 'sql') {
622
            $headers['sql'] = true;
623
            if (isset($this->options['data_style'])) {
624
                $headers['data_style'] = true;
625
            }
626
            // Set some options in database server
627
            $this->driver->execute("SET time_zone = '+00:00'");
628
            $this->driver->execute("SET sql_mode = ''");
629
        }
630
        return $headers;
631
    }
632
633
    /**
634
     * Export databases
635
     *
636
     * @param array  $databases     The databases to dump
637
     * @param array  $options       The export options
638
     *
639
     * @return array|string
640
     */
641
    public function exportDatabases(array $databases, array $options): array|string
642
    {
643
        // From dump.inc.php
644
        // $tables = array_flip($options['tables']) + array_flip($options['data']);
645
        // $ext = dump_headers((count($tables) == 1 ? key($tables) : DB), (DB == '' || count($tables) > 1));
646
        $this->options = $options;
647
        // Export to SQL format (renamed from is_sql to to_sql).
648
        $this->options['to_sql'] = preg_match('~sql~', $options['format']) === 1;
649
650
        $headers = !$this->options['to_sql'] ? null : $this->getDatabaseExportHeaders();
651
652
        foreach ($databases as $database => $tables) {
653
            try {
654
                $this->dumpDatabase($database, $tables);
655
            }
656
            catch (Exception $e) {
657
                return $e->getMessage();
658
            }
659
        }
660
661
        if ($this->options['to_sql']) {
662
            $this->queries[] = '-- ' . $this->driver->result('SELECT NOW()');
663
        }
664
665
        return [
666
            'headers' => $headers,
667
            'queries' => $this->queries,
668
        ];
669
    }
670
}
671