Test Failed
Push — main ( 31a78f...703d27 )
by Rafael
10:23
created

SqlMySql::_normalizeDbField()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 8
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
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
        $unsigned = $data['unsigned'] ?? false;
0 ignored issues
show
Unused Code introduced by
The assignment to $unsigned is dead and can be removed.
Loading history...
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
            case Schema::TYPE_STRING:
233
            case Schema::TYPE_TEXT:
234
            case Schema::TYPE_DATE:
235
            case Schema::TYPE_TIME:
236
            case Schema::TYPE_DATETIME:
237
            case Schema::TYPE_BOOLEAN:
238
                $type = 'tinyint(1)';
239
                break;
240
        }
241
        $result['Type'] = $type;
242
        $result['Null'] = !isset($data['nullable']) || $data['nullable'] ? 'YES' : 'NO';
243
        $result['Key'] = $data['type'] === 'autoincrement' ? 'PRI' : '';
244
        $result['Default'] = $data['default'] ?? null;
245
        $result['Extra'] = $data['type'] === 'autoincrement' ? 'auto_increment' : '';
246
        return $result;
247
    }
248
249
    public static function getSqlField(array $column): string
250
    {
251
        $field = $column['Field'];
252
        $type = $column['Type'];
253
        $null = $column['Null'];
254
        $key = $column['Key'];
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
255
        $default = $column['Default'];
256
        $extra = $column['Extra'];
257
258
        $sql = self::quoteTableName($field) . ' ' . $type;
259
        $nulo = ($null === 'YES');
260
        if ($extra === 'auto_increment') {
261
            $nulo = false;
262
            $sql .= ' PRIMARY KEY AUTO_INCREMENT';
263
        }
264
265
        $sql .= ($nulo ? '' : ' NOT') . ' NULL';
266
267
        $defecto = '';
268
        if (isset($default)) {
269
            if ($default === 'CURRENT_TIMESTAMP') {
270
                $defecto = $default;
271
            } elseif (is_bool($default)) {
272
                $defecto = $default ? 1 : 0;
273
            } else {
274
                $defecto = "'$defecto'";
275
            }
276
        } else {
277
            if ($nulo) {
278
                $defecto = 'NULL';
279
            }
280
        }
281
282
        if (!empty($defecto)) {
283
            $sql .= ' DEFAULT ' . $defecto;
284
        }
285
        return $sql;
286
    }
287
288
    public static function _dbFieldToSchema(array $data): array
289
    {
290
        return $data;
291
    }
292
293
    public static function _dbFieldToYaml(array $data): array
294
    {
295
        return $data;
296
    }
297
298
    /**
299
     * Recibiendo un array con los datos de un campo tal y como lo retorna la base de
300
     * datos, devuelve la información normalizada para ser utilizada por Schema.
301
     *
302
     * @author  Rafael San José Tovar <[email protected]>
303
     * @version 2023.0108
304
     *
305
     * @param array $row
306
     *
307
     * @return array
308
     */
309
    public static function _normalizeDbField(array $row): array
310
    {
311
        $result = [];
312
        $result['Field'] = $row['key'];
313
        $result['Type'] = $row['type'];
314
        $result['Null'] = $row['nullable'] ? 'YES' : 'NO';
315
        $result['Key'] = $row['type'] === 'autoincrement' ? 'PRI' : '';
316
        $result['Default'] = $row['default'] ?? null;
317
        $result['Extra'] = $row['type'] === 'autoincrement' ? 'auto_increment' : '';
318
        return $result;
319
    }
320
321
    /**
322
     * Divide the data type of a MySQL field into its various components: type,
323
     * length, unsigned or zerofill, if applicable.
324
     *
325
     * @param string $originalType
326
     *
327
     * @return array
328
     */
329
    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...
330
    {
331
        $explode = explode(' ', strtolower($originalType));
332
333
        $pos = strpos($explode[0], '(');
334
335
        $type = $pos ? substr($explode[0], 0, $pos) : $explode[0];
336
        $length = $pos ? intval(substr($explode[0], $pos + 1)) : null;
337
338
        $pos = array_search('unsigned', $explode);
339
        $unsigned = $pos ? 'unsigned' : null;
340
341
        $pos = array_search('zerofill', $explode);
342
        $zerofill = $pos ? 'zerofill' : null;
343
344
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
345
    }
346
347
    /**
348
     * Returns an array with the index information, and if there are, also constraints.
349
     *
350
     * @param array $row
351
     *
352
     * @return array
353
     */
354
    public function _normalizeIndexes(array $row): array
355
    {
356
        $result = [];
357
        $result['index'] = $row['Key_name'];
358
        $result['column'] = $row['Column_name'];
359
        $result['unique'] = $row['Non_unique'] == '0' ? 1 : 0;
360
        $result['nullable'] = $row['Null'] == 'YES' ? 1 : 0;
361
        $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

361
        /** @scrutinizer ignore-call */ 
362
        $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...
362
        if (count($constrait) > 0) {
363
            $result['constraint'] = $constrait[0]['CONSTRAINT_NAME'];
364
            $result['referencedtable'] = $constrait[0]['REFERENCED_TABLE_NAME'];
365
            $result['referencedfield'] = $constrait[0]['REFERENCED_COLUMN_NAME'];
366
        }
367
        $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

367
        /** @scrutinizer ignore-call */ 
368
        $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...
368
        if (count($constrait) > 0) {
369
            $result['matchoption'] = $constrait[0]['MATCH_OPTION'];
370
            $result['updaterule'] = $constrait[0]['UPDATE_RULE'];
371
            $result['deleterule'] = $constrait[0]['DELETE_RULE'];
372
        }
373
        return $result;
374
    }
375
376
    /**
377
     * The data about the constraint that is found in the KEY_COLUMN_USAGE table
378
     * is returned.
379
     * Attempting to return the consolidated data generates an extremely slow query
380
     * in some MySQL installations, so 2 additional simple queries are made.
381
     *
382
     * @param string $tableName
383
     * @param string $constraintName
384
     *
385
     * @return array
386
     */
387
    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...
388
    {
389
        $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

389
        $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...
390
391
        return DB::select('
392
SELECT
393
	TABLE_NAME,
394
	COLUMN_NAME,
395
	CONSTRAINT_NAME,
396
	REFERENCED_TABLE_NAME,
397
	REFERENCED_COLUMN_NAME
398
FROM
399
	INFORMATION_SCHEMA.KEY_COLUMN_USAGE
400
WHERE
401
	TABLE_SCHEMA = ' . $this->quoteFieldName($dbName) . ' AND
402
	TABLE_NAME = ' . $this->quoteFieldName($tableName) . ' AND
403
	constraint_name = ' . $this->quoteFieldName($constraintName) . ' AND
404
	REFERENCED_COLUMN_NAME IS NOT NULL;
405
        ');
406
    }
407
408
    /**
409
     * The rules for updating and deleting data with constraint (table
410
     * REFERENTIAL_CONSTRAINTS) are returned.
411
     * Attempting to return the consolidated data generates an extremely slow query
412
     * in some MySQL installations, so 2 additional simple queries are made.
413
     *
414
     * @param string $tableName
415
     * @param string $constraintName
416
     *
417
     * @return array
418
     */
419
    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...
420
    {
421
        $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

421
        $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...
422
423
        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

423
        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...
424
SELECT
425
	MATCH_OPTION,
426
	UPDATE_RULE,
427
	DELETE_RULE
428
FROM information_schema.REFERENTIAL_CONSTRAINTS
429
WHERE
430
	constraint_schema = ' . $this->quoteFieldName($dbName) . ' AND
431
	table_name = ' . $this->quoteFieldName($tableName) . ' AND
432
	constraint_name = ' . $this->quoteFieldName($constraintName) . ';
433
        ');
434
    }
435
436
    /**
437
     * Obtain an array with the basic information about the indexes of the table,
438
     * which will be supplemented with the restrictions later.
439
     *
440
     * @param string $tableName
441
     *
442
     * @return string
443
     */
444
    public function _getIndexesSql(string $tableName): string
445
    {
446
        // https://stackoverflow.com/questions/5213339/how-to-see-indexes-for-a-database-or-table-in-mysql
447
448
        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

448
        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...
449
    }
450
451
    public static function _modify(string $tableName, array $oldField, array $newField): string
452
    {
453
        $sql = 'ALTER TABLE ' . self::quoteTableName($tableName) . ' CHANGE ' . $oldField['Field'] . ' ' . $newField['Field'] . ' ';
454
        $sql .= $newField['Type'] . ' ';
455
        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...
456
            if ($oldField['Null'] === 'NO') {
457
                $sql .= 'NOT ';
458
            }
459
        }
460
        $sql .= 'NULL';
461
        if ($newField['Default'] !== null) {
462
            $sql .= ' DEFAULT "' . $newField['Default'] . '"';
463
        }
464
        $sql .= ';';
465
466
        return $sql;
467
    }
468
}
469