Passed
Push — main ( e3de8f...bc0c36 )
by Rafael
05:31
created

SqlMySql::normalizeYamlField()   F

Complexity

Conditions 23
Paths > 20000

Size

Total Lines 155
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 23
eloc 81
c 1
b 0
f 0
nc 31392
nop 1
dl 0
loc 155
rs 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A SqlMySql::modify() 0 16 4
A SqlMySql::getConstraintRules() 0 14 1
A SqlMySql::getIndexesSql() 0 5 1
A SqlMySql::normalizeIndexes() 0 20 5
A SqlMySql::getConstraintData() 0 17 1

How to fix   Long Method    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\Utils\ArrayUtils;
10
use Alxarafe\Core\Singletons\Config;
11
use Alxarafe\Core\Singletons\DebugTool;
0 ignored issues
show
Bug introduced by
The type Alxarafe\Core\Singletons\DebugTool was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 = Config::$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
    /**
125
     * Retorna un array asociativo con la información de cada columna de la tabla.
126
     * El resultado será dependiente del motor de base de datos.
127
     *
128
     * @author  Rafael San José Tovar <[email protected]>
129
     * @version 2023.0108
130
     *
131
     * @param string $tableName
132
     *
133
     * @return array
134
     */
135
    public static function getColumns(string $tableName): array
136
    {
137
        $query = 'SHOW COLUMNS FROM ' . self::quoteTableName($tableName) . ';';
138
        $rows = DB::select($query);
139
        $result = [];
140
        foreach ($rows as $row) {
141
            $result[$row['Field']] = $row;
142
        }
143
        dump($rows);
144
        return $result;
145
    }
146
147
    public static function yamlFieldToSchema(array $data): array
148
    {
149
        $column = [];
150
        $key = (string) $data['key'];
151
        $type = (string) $data['type'];
152
        $column['key'] = $key;
153
154
        /**
155
         * Entrada:
156
         * - type es el tipo lógico del campo y tiene que estar definido como índice en
157
         *   TYPES, o ser uno de los predefinidos como 'autoincrement', 'relationship', etc.
158
         *
159
         * Salida:
160
         * - type queda intacto.
161
         * - dbtype es como queda definido en la tabla, por ejemplo, varchar(20)
162
         * - realtype es el tipo resultado, por ejemplo varchar (sin el tamaño)
163
         * - generictype es uno de los índices de TYPE. P.E. autoincrement se cambiará por integer
164
         *
165
         */
166
167
        $column['type'] = $type;
168
        switch ($type) {
169
            case 'autoincrement':
170
            case 'relationship':
171
                $colType = DB::getIndexType();
172
                break;
173
            default:
174
                $colType = $type;
175
        }
176
177
        $typeArray = static::splitType($colType);
178
        /**
179
         * ^ array:4 [▼
180
         *        "type" => "bigint"
181
         *        "length" => null
182
         *        "unsigned" => "yes"
183
         *        "zerofill" => "no"
184
         * ]
185
         */
186
187
        $type = $typeArray['type'];
188
        $length = $typeArray['length'] ?? $data['length'];
189
        $unsigned = $typeArray['unsigned'] === 'yes';
190
        $zerofill = $typeArray['zerofill'] === 'yes';
0 ignored issues
show
Unused Code introduced by
The assignment to $zerofill is dead and can be removed.
Loading history...
191
        $genericType = Schema::getTypeOf($type);
192
193
        $column['dbtype'] = $colType;
194
        $column['realtype'] = $type;
195
        $column['generictype'] = $genericType;
196
197
        $column['null'] = 'YES';
198
        if ($data['null'] && mb_strtolower($data['null']) == 'no') {
199
            $column['null'] = 'NO';
200
        }
201
202
        if (empty($data['default'])) {
203
            $column['default'] = null;
204
        } else {
205
            $column['default'] = (string) $data['default'];
206
        }
207
208
        /**
209
         * Pueden existir otras definiciones de limitaciones físicas como min y max
210
         * De existir, tienen que ser contempladas en el método test y tener mayor peso que
211
         * la limitación en plantilla.
212
         */
213
        foreach (['min', 'max'] as $field) {
214
            if (isset($data[$field])) {
215
                $column[$field] = (string) $data[$field];
216
            }
217
        }
218
219
        if (isset($data['comment'])) {
220
            $column['comentario'] = (string) $data['comment'];
221
        }
222
223
        if (isset($data['default'])) {
224
            if (is_bool($data['default'])) {
225
                $column['default'] = $data['default'] ? '1' : '0';
226
            } else {
227
                $column['default'] = trim($data['default'], " \"'`");
228
            }
229
        }
230
231
        switch ($genericType) {
232
            case 'text':
233
                $column['dbtype'] = 'varchar(' . $length . ')';
234
                $column['maxlength'] = $length;
235
                break;
236
            case 'integer':
237
                /**
238
                 * Lo primero es ver la capacidad física máxima según el tipo de dato.
239
                 */
240
                $bytes = 4;
241
                switch ($type) {
242
                    case 'tinyint':
243
                        $bytes = 1;
244
                        break;
245
                    case 'smallint':
246
                        $bytes = 2;
247
                        break;
248
                    case 'mediumint':
249
                        $bytes = 3;
250
                        break;
251
                    case 'int':
252
                        $bytes = 4;
253
                        break;
254
                    case 'bigint':
255
                        $bytes = 8;
256
                        break;
257
                }
258
                $bits = 8 * (int) $bytes;
259
                $physicalMaxLength = 2 ** $bits;
260
261
                /**
262
                 * $minDataLength y $maxDataLength contendrán el mínimo y máximo valor que puede contener el campo.
263
                 */
264
                $minDataLength = $unsigned ? 0 : -$physicalMaxLength / 2;
265
                $maxDataLength = ($unsigned ? $physicalMaxLength : $physicalMaxLength / 2) - 1;
266
267
                /**
268
                 * De momento, se asignan los límites máximos por el tipo de dato.
269
                 * En $min y $max, iremos arrastrando los límites conforme se vayan comprobando.
270
                 * $min nunca podrá ser menor que $minDataLength.
271
                 * $max nunca podrá ser mayor que $maxDataLength.
272
                 */
273
                $min = $minDataLength;
274
                $max = $maxDataLength;
275
276
                /**
277
                 * Se puede hacer una limitación física Se puede haber definido en el xml un min y un max.
278
                 * A todos los efectos, lo definido en el XML como min o max se toma como limitación
279
                 * física del campo.
280
                 */
281
                if (isset($data['min'])) {
282
                    $minXmlLength = $data['min'];
283
                    if ($minXmlLength > $minDataLength) {
284
                        $min = $minXmlLength;
285
                    } else {
286
                        Debug::message("({$key}): Se ha especificado un min {$minXmlLength} en el XML, pero por el tipo de datos, el mínimo es {$minDataLength}.");
0 ignored issues
show
Bug introduced by
The type Alxarafe\Database\SqlHelpers\Debug was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
287
                    }
288
                }
289
                if (isset($data['max'])) {
290
                    $maxXmlLength = $data['max'];
291
                    if ($maxXmlLength < $maxDataLength) {
292
                        $max = $maxXmlLength;
293
                    } else {
294
                        Debug::message("({$key}): Se ha especificado un min {$maxXmlLength} en el XML, pero por el tipo de datos, el máximo es {$maxDataLength}.");
295
                    }
296
                }
297
298
                $column['min'] = $min;
299
                $column['max'] = $max;
300
                break;
301
            default:
302
                // ???
303
        }
304
        return $column;
305
    }
306
307
    public static function yamlFieldToDb(array $data): array
308
    {
309
        $result = [];
310
        $result['Field'] = $data['key'];
311
        $result['Type'] = $data['dbtype'];
312
        $result['Null'] = !isset($data['nullable']) || $data['nullable'] ? 'YES' : 'NO';
313
        $result['Key'] = $data['type'] === 'autoincrement' ? 'PRI' : '';
314
        $result['Default'] = $data['default'] ?? null;
315
        $result['Extra'] = $data['type'] === 'autoincrement' ? 'auto_increment' : '';
316
        return $result;
317
    }
318
319
    public static function dbFieldToSchema(array $data): array
320
    {
321
        return $data;
322
    }
323
324
    public static function dbFieldToYaml(array $data): array
325
    {
326
        return $data;
327
    }
328
329
    /**
330
     * Recibiendo un array con los datos de un campo tal y como lo retorna la base de
331
     * datos, devuelve la información normalizada para ser utilizada por Schema.
332
     *
333
     * @author  Rafael San José Tovar <[email protected]>
334
     * @version 2023.0108
335
     *
336
     * @param array $row
337
     *
338
     * @return array
339
     */
340
    public static function normalizeDbField(array $row): array
341
    {
342
        $result = [];
343
        $result['Field'] = $row['key'];
344
        $result['Type'] = $row['type'];
345
        $result['Null'] = $row['nullable'] ? 'YES' : 'NO';
346
        $result['Key'] = $row['type'] === 'autoincrement' ? 'PRI' : '';
347
        $result['Default'] = $row['default'] ?? null;
348
        $result['Extra'] = $row['type'] === 'autoincrement' ? 'auto_increment' : '';
349
        return $result;
350
    }
351
352
    /**
353
     * Divide the data type of a MySQL field into its various components: type,
354
     * length, unsigned or zerofill, if applicable.
355
     *
356
     * @param string $originalType
357
     *
358
     * @return array
359
     */
360
    private static function splitType(string $originalType): array
361
    {
362
        $explode = explode(' ', strtolower($originalType));
363
364
        $pos = strpos($explode[0], '(');
365
366
        $type = $pos ? substr($explode[0], 0, $pos) : $explode[0];
367
        $length = $pos ? intval(substr($explode[0], $pos + 1)) : null;
368
369
        $pos = array_search('unsigned', $explode);
370
        $unsigned = $pos ? 'unsigned' : null;
371
372
        $pos = array_search('zerofill', $explode);
373
        $zerofill = $pos ? 'zerofill' : null;
374
375
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
376
    }
377
378
    /**
379
     * Returns an array with the index information, and if there are, also constraints.
380
     *
381
     * @param array $row
382
     *
383
     * @return array
384
     */
385
    public function normalizeIndexes(array $row): array
386
    {
387
        $result = [];
388
        $result['index'] = $row['Key_name'];
389
        $result['column'] = $row['Column_name'];
390
        $result['unique'] = $row['Non_unique'] == '0' ? 1 : 0;
391
        $result['nullable'] = $row['Null'] == 'YES' ? 1 : 0;
392
        $constrait = $this->getConstraintData($row['Table'], $row['Key_name']);
393
        if (count($constrait) > 0) {
394
            $result['constraint'] = $constrait[0]['CONSTRAINT_NAME'];
395
            $result['referencedtable'] = $constrait[0]['REFERENCED_TABLE_NAME'];
396
            $result['referencedfield'] = $constrait[0]['REFERENCED_COLUMN_NAME'];
397
        }
398
        $constrait = $this->getConstraintRules($row['Table'], $row['Key_name']);
399
        if (count($constrait) > 0) {
400
            $result['matchoption'] = $constrait[0]['MATCH_OPTION'];
401
            $result['updaterule'] = $constrait[0]['UPDATE_RULE'];
402
            $result['deleterule'] = $constrait[0]['DELETE_RULE'];
403
        }
404
        return $result;
405
    }
406
407
    /**
408
     * The data about the constraint that is found in the KEY_COLUMN_USAGE table
409
     * is returned.
410
     * Attempting to return the consolidated data generates an extremely slow query
411
     * in some MySQL installations, so 2 additional simple queries are made.
412
     *
413
     * @param string $tableName
414
     * @param string $constraintName
415
     *
416
     * @return array
417
     */
418
    private function getConstraintData(string $tableName, string $constraintName): array
419
    {
420
        $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

420
        $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...
421
422
        return DB::select('
423
SELECT
424
	TABLE_NAME,
425
	COLUMN_NAME,
426
	CONSTRAINT_NAME,
427
	REFERENCED_TABLE_NAME,
428
	REFERENCED_COLUMN_NAME
429
FROM
430
	INFORMATION_SCHEMA.KEY_COLUMN_USAGE
431
WHERE
432
	TABLE_SCHEMA = ' . $this->quoteFieldName($dbName) . ' AND
433
	TABLE_NAME = ' . $this->quoteFieldName($tableName) . ' AND
434
	constraint_name = ' . $this->quoteFieldName($constraintName) . ' AND
435
	REFERENCED_COLUMN_NAME IS NOT NULL;
436
        ');
437
    }
438
439
    /**
440
     * The rules for updating and deleting data with constraint (table
441
     * REFERENTIAL_CONSTRAINTS) are returned.
442
     * Attempting to return the consolidated data generates an extremely slow query
443
     * in some MySQL installations, so 2 additional simple queries are made.
444
     *
445
     * @param string $tableName
446
     * @param string $constraintName
447
     *
448
     * @return array
449
     */
450
    private function getConstraintRules(string $tableName, string $constraintName): array
451
    {
452
        $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

452
        $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...
453
454
        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

454
        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...
455
SELECT
456
	MATCH_OPTION,
457
	UPDATE_RULE,
458
	DELETE_RULE
459
FROM information_schema.REFERENTIAL_CONSTRAINTS
460
WHERE
461
	constraint_schema = ' . $this->quoteFieldName($dbName) . ' AND
462
	table_name = ' . $this->quoteFieldName($tableName) . ' AND
463
	constraint_name = ' . $this->quoteFieldName($constraintName) . ';
464
        ');
465
    }
466
467
    /**
468
     * Obtain an array with the basic information about the indexes of the table,
469
     * which will be supplemented with the restrictions later.
470
     *
471
     * @param string $tableName
472
     *
473
     * @return string
474
     */
475
    public function getIndexesSql(string $tableName): string
476
    {
477
        // https://stackoverflow.com/questions/5213339/how-to-see-indexes-for-a-database-or-table-in-mysql
478
479
        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

479
        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...
480
    }
481
482
    public static function modify(string $tableName, array $oldField, array $newField): string
483
    {
484
        $sql = 'ALTER TABLE ' . self::quoteTableName($tableName) . ' CHANGE ' . $oldField['Field'] . ' ' . $newField['Field'] . ' ';
485
        $sql .= $newField['Type'] . ' ';
486
        if ($newField) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newField of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
487
            if ($oldField['Null'] === 'NO') {
488
                $sql .= 'NOT ';
489
            }
490
        }
491
        $sql .= 'NULL';
492
        if ($newField['Default'] !== null) {
493
            $sql .= ' DEFAULT "' . $newField['Default'] . '"';
494
        }
495
        $sql .= ';';
496
497
        return $sql;
498
    }
499
}
500