Test Failed
Push — main ( a17396...e328ab )
by Rafael
05:43
created

SqlMySql::sanitizeDbStructure()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 19
c 1
b 0
f 0
nc 10
nop 2
dl 0
loc 25
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Alxarafe. Development of PHP applications in a flash!
4
 * Copyright (C) 2018 Alxarafe <[email protected]>
5
 */
6
7
namespace Alxarafe\Database\SqlHelpers;
8
9
use Alxarafe\Core\Singletons\Debug;
10
use Alxarafe\Core\Utils\ArrayUtils;
11
use Alxarafe\Core\Singletons\Config;
12
use Alxarafe\Database\DB;
13
use Alxarafe\Database\Schema;
14
use Alxarafe\Database\SqlHelper;
15
16
/**
17
 * Class SqlMySql
18
 *
19
 * Soporte específico para la creación de comandos y consultas usando el motor MySQL.
20
 * Es usado directamente por la clase estática DB.
21
 *
22
 * @author  Rafael San José Tovar <[email protected]>
23
 * @version 2023.0108
24
 *
25
 * @package Alxarafe\Database\SqlHelpers
26
 */
27
class SqlMySql extends SqlHelper
28
{
29
    /**
30
     * Retorna las comillas que encierran al nombre de la tabla en una consulta SQL.
31
     *
32
     * @author  Rafael San José Tovar <[email protected]>
33
     * @version 2023.0108
34
     *
35
     * @return string
36
     */
37
    public static function getTableQuote(): string
38
    {
39
        return '`';
40
    }
41
42
    /**
43
     * Retorna las comillas que encierran al nombre de un campo en una consulta SQL
44
     *
45
     * @author  Rafael San José Tovar <[email protected]>
46
     * @version 2023.0108
47
     *
48
     * @return string
49
     */
50
    public static function getFieldQuote(): string
51
    {
52
        return '"';
53
    }
54
55
    /**
56
     * Retorna true si la tabla existe en la base de datos.
57
     *
58
     * @author  Rafael San José Tovar <[email protected]>
59
     * @version 2023.0106
60
     *
61
     * @param string $tableName
62
     *
63
     * @return bool
64
     */
65
    public static function tableExists(string $tableName): bool
66
    {
67
        $dbName = DB::$dbName;
68
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableName}'";
69
70
        $data = DB::select($sql);
71
        $result = reset($data);
72
73
        return $result['Total'] === '1';
74
    }
75
76
    /**
77
     * Retorna un array con la asociación de tipos del motor SQL para cada tipo definido
78
     * en el Schema.
79
     *
80
     * @author  Rafael San José Tovar <[email protected]>
81
     * @version 2023.0108
82
     *
83
     * @return array[]
84
     */
85
    public static function getDataTypes(): array
86
    {
87
        return [
88
            Schema::TYPE_INTEGER => ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'],
89
            Schema::TYPE_FLOAT => ['real', 'double'],
90
            Schema::TYPE_DECIMAL => ['decimal', 'numeric'],
91
            Schema::TYPE_STRING => ['char', 'varchar'],
92
            Schema::TYPE_TEXT => ['tinytext', 'text', 'mediumtext', 'longtext', 'blob'],
93
            Schema::TYPE_DATE => ['date'],
94
            Schema::TYPE_TIME => ['time'],
95
            Schema::TYPE_DATETIME => ['datetime', 'timestamp'],
96
            Schema::TYPE_BOOLEAN => ['boolean'],
97
        ];
98
    }
99
100
    /**
101
     * Retorna un array con el nombre de todas las tablas de la base de datos.
102
     *
103
     * @return array
104
     */
105
    public static function getTables(): array
106
    {
107
        $query = 'SHOW TABLES';
108
        return ArrayUtils::flatArray(DB::select($query));
109
    }
110
111
    /**
112
     * Retorna el tipo de dato que se utiliza para los índices autoincrementados
113
     *
114
     * @author  Rafael San José Tovar <[email protected]>
115
     * @version 2023.0108
116
     *
117
     * @return string
118
     */
119
    public static function getIndexType(): string
120
    {
121
        return 'bigint(20) unsigned';
122
    }
123
124
    public static function getIntegerMinMax(int $size, bool $unsigned): array
125
    {
126
        switch ($size) {
127
            case 1:
128
                $type = 'tinyint';
129
                break;
130
            case 2:
131
                $type = 'smallint';
132
                break;
133
            case 3:
134
                $type = 'mediumint';
135
                break;
136
            case 4:
137
                $type = 'int';
138
                break;
139
            default:
140
                $type = 'bigint';
141
                $size = 8;
142
                break;
143
        }
144
145
        $bits = 8 * (int) $size;
146
        $physicalMaxLength = 2 ** $bits;
147
148
        /**
149
         * $minDataLength y $maxDataLength contendrán el mínimo y máximo valor que puede contener el campo.
150
         */
151
        $minDataLength = $unsigned ? 0 : -$physicalMaxLength / 2;
152
        $maxDataLength = ($unsigned ? $physicalMaxLength : $physicalMaxLength / 2) - 1;
153
154
        /**
155
         * De momento, se asignan los límites máximos por el tipo de dato.
156
         * En $min y $max, iremos arrastrando los límites conforme se vayan comprobando.
157
         * $min nunca podrá ser menor que $minDataLength.
158
         * $max nunca podrá ser mayor que $maxDataLength.
159
         */
160
        $min = $minDataLength;
161
        $max = $maxDataLength;
162
163
        return [
164
            'dbtype' => $type,
165
            'min' => $min,
166
            'max' => $max,
167
            'size' => $size,
168
            'unsigned' => $unsigned,
169
        ];
170
    }
171
172
    /**
173
     * Retorna un array asociativo con la información de cada columna de la tabla.
174
     * El resultado será dependiente del motor de base de datos.
175
     *
176
     * @author  Rafael San José Tovar <[email protected]>
177
     * @version 2023.0108
178
     *
179
     * @param string $tableName
180
     *
181
     * @return array
182
     */
183
    public static function getColumns(string $tableName): array
184
    {
185
        $query = 'SHOW COLUMNS FROM ' . self::quoteTableName($tableName) . ';';
186
        $rows = DB::select($query);
187
        $result = [];
188
        foreach ($rows as $row) {
189
            $result[$row['Field']] = $row;
190
        }
191
        return $result;
192
    }
193
194
    public static function yamlFieldIntegerToDb(array $data): string
195
    {
196
        $type = $data['dbtype'];
197
        // TODO: Aunque lo que está comentado va, igual no hace falta si al comparar
198
        //       ignoramos el tamaño a mostrar para los integer
199
200
        /*
201
        $unsigned = $data['unsigned'] ?? false;
202
        switch ($type) {
203
            case 'tinyint':
204
                return $type . ($unsigned ? '(3) unsigned' : '(4)');
205
            case 'smallint':
206
                break;
207
            case 'mediumint':
208
                break;
209
            case 'int':
210
                return $type . ($unsigned ? '(10) unsigned' : '(11)');
211
            case 'bigint':
212
                $type .= '(20)';
213
        }
214
        */
215
        return $type . ($unsigned ? ' unsigned' : '');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $unsigned seems to be never defined.
Loading history...
216
    }
217
218
    public static function yamlFieldToDb(array $data): array
219
    {
220
        $nullable = strtolower($data['nullable']) !== 'no';
221
222
        $result = [];
223
        $result['Field'] = $data['name'];
224
225
        $type = $data['dbtype'];
226
        switch ($data['generictype']) {
227
            case Schema::TYPE_INTEGER:
228
                $type = self::yamlFieldIntegerToDb($data);
229
                break;
230
            case Schema::TYPE_FLOAT:
231
            case Schema::TYPE_DECIMAL:
232
                break;
233
            case Schema::TYPE_STRING:
234
                $type = 'varchar(' . $data['length'] . ')';
235
                break;
236
            case Schema::TYPE_TEXT:
237
            case Schema::TYPE_DATE:
238
            case Schema::TYPE_TIME:
239
            case Schema::TYPE_DATETIME:
240
                break;
241
            case Schema::TYPE_BOOLEAN:
242
                //                $type = 'tinyint(1)';
243
                break;
244
        }
245
        $result['Type'] = $type;
246
        $result['Null'] = $nullable ? 'YES' : 'NO';
247
        $result['Key'] = $data['type'] === 'autoincrement' ? 'PRI' : '';
248
        $result['Default'] = $data['default'] ?? null;
249
        $result['Extra'] = $data['type'] === 'autoincrement' ? 'auto_increment' : '';
250
        return $result;
251
    }
252
253
    public static function getSqlField(array $column): string
254
    {
255
        $field = $column['Field'];
256
        $type = $column['Type'];
257
        $null = $column['Null'];
258
        $key = $column['Key'];
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
259
        $default = $column['Default'];
260
        $extra = $column['Extra'];
261
262
        $sql = self::quoteTableName($field) . ' ' . $type;
263
        $nulo = ($null === 'YES');
264
        if ($extra === 'auto_increment') {
265
            $nulo = false;
266
            $sql .= ' PRIMARY KEY AUTO_INCREMENT';
267
        }
268
269
        $sql .= ($nulo ? '' : ' NOT') . ' NULL';
270
271
        $defecto = '';
272
        if (isset($default)) {
273
            if ($default === 'CURRENT_TIMESTAMP') {
274
                $defecto = $default;
275
            } elseif (is_bool($default)) {
276
                $defecto = $default ? 1 : 0;
277
            } else {
278
                $defecto = "'$defecto'";
279
            }
280
        } else {
281
            if ($nulo) {
282
                $defecto = 'NULL';
283
            }
284
        }
285
286
        if (!empty($defecto)) {
287
            $sql .= ' DEFAULT ' . $defecto;
288
        }
289
        return $sql;
290
    }
291
292
    public static function _dbFieldToSchema(array $data): array
293
    {
294
        return $data;
295
    }
296
297
    public static function _dbFieldToYaml(array $data): array
298
    {
299
        return $data;
300
    }
301
302
    /**
303
     * Recibiendo un array con los datos de un campo tal y como lo retorna la base de
304
     * datos, devuelve la información normalizada para ser utilizada por Schema.
305
     *
306
     * @author  Rafael San José Tovar <[email protected]>
307
     * @version 2023.0108
308
     *
309
     * @param array $row
310
     *
311
     * @return array
312
     */
313
    public static function _normalizeDbField(array $row): array
314
    {
315
        $result = [];
316
        $result['Field'] = $row['key'];
317
        $result['Type'] = $row['type'];
318
        $result['Null'] = $row['nullable'] ? 'YES' : 'NO';
319
        $result['Key'] = $row['type'] === 'autoincrement' ? 'PRI' : '';
320
        $result['Default'] = $row['default'] ?? null;
321
        $result['Extra'] = $row['type'] === 'autoincrement' ? 'auto_increment' : '';
322
        return $result;
323
    }
324
325
    /**
326
     * Divide the data type of a MySQL field into its various components: type,
327
     * length, unsigned or zerofill, if applicable.
328
     *
329
     * @param string $originalType
330
     *
331
     * @return array
332
     */
333
    private static function _splitType(string $originalType): array
0 ignored issues
show
Unused Code introduced by
The method _splitType() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
334
    {
335
        $explode = explode(' ', strtolower($originalType));
336
337
        $pos = strpos($explode[0], '(');
338
339
        $type = $pos ? substr($explode[0], 0, $pos) : $explode[0];
340
        $length = $pos ? intval(substr($explode[0], $pos + 1)) : null;
341
342
        $pos = array_search('unsigned', $explode);
343
        $unsigned = $pos ? 'unsigned' : null;
344
345
        $pos = array_search('zerofill', $explode);
346
        $zerofill = $pos ? 'zerofill' : null;
347
348
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
349
    }
350
351
    /**
352
     * Returns an array with the index information, and if there are, also constraints.
353
     *
354
     * @param array $row
355
     *
356
     * @return array
357
     */
358
    public function _normalizeIndexes(array $row): array
359
    {
360
        $result = [];
361
        $result['index'] = $row['Key_name'];
362
        $result['column'] = $row['Column_name'];
363
        $result['unique'] = $row['Non_unique'] == '0' ? 1 : 0;
364
        $result['nullable'] = $row['Null'] == 'YES' ? 1 : 0;
365
        $constrait = $this->getConstraintData($row['Table'], $row['Key_name']);
0 ignored issues
show
Bug introduced by
The method getConstraintData() does not exist on Alxarafe\Database\SqlHelpers\SqlMySql. Did you maybe mean _getConstraintData()? ( Ignorable by Annotation )

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

365
        /** @scrutinizer ignore-call */ 
366
        $constrait = $this->getConstraintData($row['Table'], $row['Key_name']);

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...
366
        if (count($constrait) > 0) {
367
            $result['constraint'] = $constrait[0]['CONSTRAINT_NAME'];
368
            $result['referencedtable'] = $constrait[0]['REFERENCED_TABLE_NAME'];
369
            $result['referencedfield'] = $constrait[0]['REFERENCED_COLUMN_NAME'];
370
        }
371
        $constrait = $this->getConstraintRules($row['Table'], $row['Key_name']);
0 ignored issues
show
Bug introduced by
The method getConstraintRules() does not exist on Alxarafe\Database\SqlHelpers\SqlMySql. Did you maybe mean _getConstraintRules()? ( Ignorable by Annotation )

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

371
        /** @scrutinizer ignore-call */ 
372
        $constrait = $this->getConstraintRules($row['Table'], $row['Key_name']);

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...
372
        if (count($constrait) > 0) {
373
            $result['matchoption'] = $constrait[0]['MATCH_OPTION'];
374
            $result['updaterule'] = $constrait[0]['UPDATE_RULE'];
375
            $result['deleterule'] = $constrait[0]['DELETE_RULE'];
376
        }
377
        return $result;
378
    }
379
380
    /**
381
     * The data about the constraint that is found in the KEY_COLUMN_USAGE table
382
     * is returned.
383
     * Attempting to return the consolidated data generates an extremely slow query
384
     * in some MySQL installations, so 2 additional simple queries are made.
385
     *
386
     * @param string $tableName
387
     * @param string $constraintName
388
     *
389
     * @return array
390
     */
391
    private function _getConstraintData(string $tableName, string $constraintName): array
0 ignored issues
show
Unused Code introduced by
The method _getConstraintData() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
392
    {
393
        $dbName = Config::getVar('dbName') ?? 'Unknown';
0 ignored issues
show
Bug introduced by
The call to Alxarafe\Core\Singletons\Config::getVar() has too few arguments starting with section. ( Ignorable by Annotation )

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

393
        $dbName = Config::/** @scrutinizer ignore-call */ getVar('dbName') ?? 'Unknown';

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
394
395
        return DB::select('
396
SELECT
397
	TABLE_NAME,
398
	COLUMN_NAME,
399
	CONSTRAINT_NAME,
400
	REFERENCED_TABLE_NAME,
401
	REFERENCED_COLUMN_NAME
402
FROM
403
	INFORMATION_SCHEMA.KEY_COLUMN_USAGE
404
WHERE
405
	TABLE_SCHEMA = ' . $this->quoteFieldName($dbName) . ' AND
406
	TABLE_NAME = ' . $this->quoteFieldName($tableName) . ' AND
407
	constraint_name = ' . $this->quoteFieldName($constraintName) . ' AND
408
	REFERENCED_COLUMN_NAME IS NOT NULL;
409
        ');
410
    }
411
412
    /**
413
     * The rules for updating and deleting data with constraint (table
414
     * REFERENTIAL_CONSTRAINTS) are returned.
415
     * Attempting to return the consolidated data generates an extremely slow query
416
     * in some MySQL installations, so 2 additional simple queries are made.
417
     *
418
     * @param string $tableName
419
     * @param string $constraintName
420
     *
421
     * @return array
422
     */
423
    private function _getConstraintRules(string $tableName, string $constraintName): array
0 ignored issues
show
Unused Code introduced by
The method _getConstraintRules() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
424
    {
425
        $dbName = Config::getVar('dbName') ?? 'Unknown';
0 ignored issues
show
Bug introduced by
The call to Alxarafe\Core\Singletons\Config::getVar() has too few arguments starting with section. ( Ignorable by Annotation )

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

425
        $dbName = Config::/** @scrutinizer ignore-call */ getVar('dbName') ?? 'Unknown';

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
426
427
        return DB::selectselect('
0 ignored issues
show
Bug introduced by
The method selectselect() does not exist on Alxarafe\Database\DB. Did you maybe mean select()? ( Ignorable by Annotation )

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

427
        return DB::/** @scrutinizer ignore-call */ selectselect('

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...
428
SELECT
429
	MATCH_OPTION,
430
	UPDATE_RULE,
431
	DELETE_RULE
432
FROM information_schema.REFERENTIAL_CONSTRAINTS
433
WHERE
434
	constraint_schema = ' . $this->quoteFieldName($dbName) . ' AND
435
	table_name = ' . $this->quoteFieldName($tableName) . ' AND
436
	constraint_name = ' . $this->quoteFieldName($constraintName) . ';
437
        ');
438
    }
439
440
    /**
441
     * Obtain an array with the basic information about the indexes of the table,
442
     * which will be supplemented with the restrictions later.
443
     *
444
     * @param string $tableName
445
     *
446
     * @return string
447
     */
448
    public function _getIndexesSql(string $tableName): string
449
    {
450
        // https://stackoverflow.com/questions/5213339/how-to-see-indexes-for-a-database-or-table-in-mysql
451
452
        return 'SHOW INDEX FROM ' . Config::getInstance()->getSqlHelper()->quoteTableName($tableName);
0 ignored issues
show
Bug introduced by
The method getInstance() does not exist on Alxarafe\Core\Singletons\Config. ( Ignorable by Annotation )

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

452
        return 'SHOW INDEX FROM ' . Config::/** @scrutinizer ignore-call */ getInstance()->getSqlHelper()->quoteTableName($tableName);

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...
453
    }
454
455
    /**
456
     * Toma la estructura de un campo obtenida de la base de datos, y la retorna
457
     * de la misma forma en la que se usó al ser creada.
458
     * Esto es necesario, porque algunas bases de datos cambian tipos como boolean por
459
     * tinyint(1), o int por int(10)
460
     *
461
     * @author Rafael San José Tovar <[email protected]>
462
     *
463
     * @param string $genericType
464
     * @param array  $structure
465
     *
466
     * @return array
467
     */
468
    public static function sanitizeDbStructure(string $genericType, array $structure): array
469
    {
470
        $type = $structure['Type'];
471
        switch ($genericType) {
472
            // Tipos que no cambian
473
            case Schema::TYPE_FLOAT:
474
            case Schema::TYPE_DECIMAL:
475
            case Schema::TYPE_STRING:
476
            case Schema::TYPE_TEXT:
477
            case Schema::TYPE_DATE:
478
            case Schema::TYPE_TIME:
479
            case Schema::TYPE_DATETIME:
480
                break;
481
            // Tipos a los que hay que quitar los paréntesis
482
            case Schema::TYPE_INTEGER:
483
                $type = preg_replace("/\((.*?)\)/i", "", $type);
484
                break;
485
            // Tipos que cambian durante la creación
486
            case Schema::TYPE_BOOLEAN:
487
                $type = 'boolean'; // Se crea como boolean y se retorna como tinyint(1)
488
                $structure['Default'] = ($structure['Default'] === '1');
489
                break;
490
        }
491
        $structure['Type'] = $type;
492
        return $structure;
493
    }
494
495
    public static function modify(string $tableName, array $oldField, array $newField): string
496
    {
497
        $sql = 'ALTER TABLE ' . self::quoteTableName($tableName) . ' CHANGE ' . $oldField['Field'] . ' ' . $newField['Field'] . ' ';
498
        $sql .= $newField['Type'] . ' ';
499
        if (strtolower($newField['Null']) === 'no') {
500
            $sql .= 'NOT ';
501
        }
502
        $sql .= 'NULL';
503
        if ($newField['Default'] !== null) {
504
            if ($newField['Type'] === 'boolean') {
505
                $newField['Default'] = $newField['Default'] ? '1' : '0';
506
            }
507
            $sql .= ' DEFAULT "' . $newField['Default'] . '"';
508
        }
509
        $sql .= ';';
510
        return $sql;
511
    }
512
}
513