Test Failed
Push — main ( a047e2...82fe17 )
by Rafael
05:55
created

SqlMySql::getIntegerMinMax()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 29
nc 20
nop 2
dl 0
loc 45
rs 8.5226
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
            unset($row['Key']);  // Los índices están gestionados por separado
190
            $result[$row['Field']] = $row;
191
        }
192
        return $result;
193
    }
194
195
    public static function yamlFieldIntegerToDb(array $data): string
196
    {
197
        $type = $data['dbtype'];
198
        // TODO: Aunque lo que está comentado va, igual no hace falta si al comparar
199
        //       ignoramos el tamaño a mostrar para los integer
200
201
        $unsigned = $data['unsigned'] ?? false;
202
        /*
203
        switch ($type) {
204
            case 'tinyint':
205
                return $type . ($unsigned ? '(3) unsigned' : '(4)');
206
            case 'smallint':
207
                break;
208
            case 'mediumint':
209
                break;
210
            case 'int':
211
                return $type . ($unsigned ? '(10) unsigned' : '(11)');
212
            case 'bigint':
213
                $type .= '(20)';
214
        }
215
        */
216
        return $type . ($unsigned ? ' unsigned' : '');
217
    }
218
219
    public static function yamlFieldToDb(array $data): array
220
    {
221
        $nullable = strtolower($data['nullable']) !== 'no';
222
223
        $result = [];
224
        $result['Field'] = $data['name'];
225
226
        $type = $data['dbtype'];
227
        switch ($data['generictype']) {
228
            case Schema::TYPE_INTEGER:
229
                $type = self::yamlFieldIntegerToDb($data);
230
                break;
231
            case Schema::TYPE_FLOAT:
232
            case Schema::TYPE_DECIMAL:
233
                break;
234
            case Schema::TYPE_STRING:
235
                $type = 'varchar(' . $data['length'] . ')';
236
                break;
237
            case Schema::TYPE_TEXT:
238
            case Schema::TYPE_DATE:
239
            case Schema::TYPE_TIME:
240
            case Schema::TYPE_DATETIME:
241
                break;
242
            case Schema::TYPE_BOOLEAN:
243
                //                $type = 'tinyint(1)';
244
                break;
245
        }
246
        $result['Type'] = $type;
247
        $result['Null'] = $nullable ? 'YES' : 'NO';
248
        $result['Key'] = $data['type'] === 'autoincrement' ? 'PRI' : '';
249
        $result['Default'] = $data['default'] ?? null;
250
        $result['Extra'] = $data['type'] === 'autoincrement' ? 'auto_increment' : '';
251
        return $result;
252
    }
253
254
    public static function yamlIndexToDb(array $data): array
255
    {
256
        $result = [];
257
        foreach ($data['fields'] as $name => $field) {
258
            if ($field['type'] === 'autoincrement') {
259
                $result['PRIMARY'] = [
260
                    'column' => $name,
261
                    'primary' => 'yes',
262
                ];
263
            }
264
        }
265
        return array_merge($result, $data['indexes'] ?? []);
266
    }
267
268
    public static function getSqlField(array $column): string
269
    {
270
        $field = $column['Field'];
271
        $type = $column['Type'];
272
        $null = $column['Null'];
273
        $key = $column['Key'];
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
274
        $default = $column['Default'];
275
        $extra = $column['Extra'];
276
277
        $sql = self::quoteTableName($field) . ' ' . $type;
278
        $nulo = ($null === 'YES');
279
        if ($extra === 'auto_increment') {
280
            $nulo = false;
281
            $sql .= ' PRIMARY KEY AUTO_INCREMENT';
282
        }
283
284
        $sql .= ($nulo ? '' : ' NOT') . ' NULL';
285
286
        $defecto = '';
287
        if (isset($default)) {
288
            if ($default === 'CURRENT_TIMESTAMP') {
289
                $defecto = $default;
290
            } elseif (is_bool($default)) {
291
                $defecto = $default ? 1 : 0;
292
            } else {
293
                $defecto = "'$defecto'";
294
            }
295
        } else {
296
            if ($nulo) {
297
                $defecto = 'NULL';
298
            }
299
        }
300
301
        if (!empty($defecto)) {
302
            $sql .= ' DEFAULT ' . $defecto;
303
        }
304
        return $sql;
305
    }
306
307
    public static function _dbFieldToSchema(array $data): array
308
    {
309
        return $data;
310
    }
311
312
    public static function _dbFieldToYaml(array $data): array
313
    {
314
        return $data;
315
    }
316
317
    /**
318
     * Recibiendo un array con los datos de un campo tal y como lo retorna la base de
319
     * datos, devuelve la información normalizada para ser utilizada por Schema.
320
     *
321
     * @author  Rafael San José Tovar <[email protected]>
322
     * @version 2023.0108
323
     *
324
     * @param array $row
325
     *
326
     * @return array
327
     */
328
    public static function _normalizeDbField(array $row): array
329
    {
330
        $result = [];
331
        $result['Field'] = $row['key'];
332
        $result['Type'] = $row['type'];
333
        $result['Null'] = $row['nullable'] ? 'YES' : 'NO';
334
        $result['Key'] = $row['type'] === 'autoincrement' ? 'PRI' : '';
335
        $result['Default'] = $row['default'] ?? null;
336
        $result['Extra'] = $row['type'] === 'autoincrement' ? 'auto_increment' : '';
337
        return $result;
338
    }
339
340
    /**
341
     * Divide the data type of a MySQL field into its various components: type,
342
     * length, unsigned or zerofill, if applicable.
343
     *
344
     * @param string $originalType
345
     *
346
     * @return array
347
     */
348
    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...
349
    {
350
        $explode = explode(' ', strtolower($originalType));
351
352
        $pos = strpos($explode[0], '(');
353
354
        $type = $pos ? substr($explode[0], 0, $pos) : $explode[0];
355
        $length = $pos ? intval(substr($explode[0], $pos + 1)) : null;
356
357
        $pos = array_search('unsigned', $explode);
358
        $unsigned = $pos ? 'unsigned' : null;
359
360
        $pos = array_search('zerofill', $explode);
361
        $zerofill = $pos ? 'zerofill' : null;
362
363
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
364
    }
365
366
    /**
367
     * Returns an array with the index information, and if there are, also constraints.
368
     *
369
     * @param array $row
370
     *
371
     * @return array
372
     */
373
    public static function normalizeIndexes(array $row): array
374
    {
375
        $result = [];
376
        $result['index'] = $row['Key_name'];
377
        $result['column'] = $row['Column_name'];
378
        $result['unique'] = $row['Non_unique'] == '0' ? 1 : 0;
379
        $result['nullable'] = $row['Null'] == 'YES' ? 1 : 0;
380
        $constrait = self::getConstraintData($row['Table'], $row['Key_name']);
381
        if (count($constrait) > 0) {
382
            $result['constraint'] = $constrait[0]['CONSTRAINT_NAME'];
383
            $result['referencedtable'] = $constrait[0]['REFERENCED_TABLE_NAME'];
384
            $result['referencedfield'] = $constrait[0]['REFERENCED_COLUMN_NAME'];
385
        }
386
        $constrait = self::getConstraintRules($row['Table'], $row['Key_name']);
387
        if (count($constrait) > 0) {
388
            $result['matchoption'] = $constrait[0]['MATCH_OPTION'];
389
            $result['updaterule'] = $constrait[0]['UPDATE_RULE'];
390
            $result['deleterule'] = $constrait[0]['DELETE_RULE'];
391
        }
392
        return $result;
393
    }
394
395
    /**
396
     * The data about the constraint that is found in the KEY_COLUMN_USAGE table
397
     * is returned.
398
     * Attempting to return the consolidated data generates an extremely slow query
399
     * in some MySQL installations, so 2 additional simple queries are made.
400
     *
401
     * @param string $tableName
402
     * @param string $constraintName
403
     *
404
     * @return array
405
     */
406
    private static function getConstraintData(string $tableName, string $constraintName): array
407
    {
408
        $dbName = DB::$dbName;
409
410
        return DB::select('
411
SELECT
412
	TABLE_NAME,
413
	COLUMN_NAME,
414
	CONSTRAINT_NAME,
415
	REFERENCED_TABLE_NAME,
416
	REFERENCED_COLUMN_NAME
417
FROM
418
	INFORMATION_SCHEMA.KEY_COLUMN_USAGE
419
WHERE
420
	TABLE_SCHEMA = ' . self::quoteFieldName($dbName) . ' AND
421
	TABLE_NAME = ' . self::quoteFieldName($tableName) . ' AND
422
	constraint_name = ' . self::quoteFieldName($constraintName) . ' AND
423
	REFERENCED_COLUMN_NAME IS NOT NULL;
424
        ');
425
    }
426
427
    /**
428
     * The rules for updating and deleting data with constraint (table
429
     * REFERENTIAL_CONSTRAINTS) are returned.
430
     * Attempting to return the consolidated data generates an extremely slow query
431
     * in some MySQL installations, so 2 additional simple queries are made.
432
     *
433
     * @param string $tableName
434
     * @param string $constraintName
435
     *
436
     * @return array
437
     */
438
    private static function getConstraintRules(string $tableName, string $constraintName): array
439
    {
440
        $dbName = DB::$dbName;
441
442
        return DB::select('
443
SELECT
444
	MATCH_OPTION,
445
	UPDATE_RULE,
446
	DELETE_RULE
447
FROM information_schema.REFERENTIAL_CONSTRAINTS
448
WHERE
449
	constraint_schema = ' . self::quoteFieldName($dbName) . ' AND
450
	table_name = ' . self::quoteFieldName($tableName) . ' AND
451
	constraint_name = ' . self::quoteFieldName($constraintName) . ';
452
        ');
453
    }
454
455
    /**
456
     * Obtain an array with the basic information about the indexes of the table,
457
     * which will be supplemented with the restrictions later.
458
     *
459
     * @param string $tableName
460
     *
461
     * @return string
462
     */
463
    public static function getIndexesSql(string $tableName): string
464
    {
465
        // https://stackoverflow.com/questions/5213339/how-to-see-indexes-for-a-database-or-table-in-mysql
466
467
        return 'SHOW INDEX FROM ' . self::quoteTableName($tableName);
468
    }
469
470
    /**
471
     * Toma la estructura de un campo obtenida de la base de datos, y la retorna
472
     * de la misma forma en la que se usó al ser creada.
473
     * Esto es necesario, porque algunas bases de datos cambian tipos como boolean por
474
     * tinyint(1), o int por int(10)
475
     *
476
     * @author Rafael San José Tovar <[email protected]>
477
     *
478
     * @param string $genericType
479
     * @param array  $structure
480
     *
481
     * @return array
482
     */
483
    public static function sanitizeDbStructure(string $genericType, array $structure): array
484
    {
485
        $type = $structure['Type'];
486
        switch ($genericType) {
487
            // Tipos que no cambian
488
            case Schema::TYPE_FLOAT:
489
            case Schema::TYPE_DECIMAL:
490
            case Schema::TYPE_STRING:
491
            case Schema::TYPE_TEXT:
492
            case Schema::TYPE_DATE:
493
            case Schema::TYPE_TIME:
494
            case Schema::TYPE_DATETIME:
495
                break;
496
            // Tipos a los que hay que quitar los paréntesis
497
            case Schema::TYPE_INTEGER:
498
                $type = preg_replace("/\((.*?)\)/i", "", $type);
499
                break;
500
            // Tipos que cambian durante la creación
501
            case Schema::TYPE_BOOLEAN:
502
                $type = 'boolean'; // Se crea como boolean y se retorna como tinyint(1)
503
                $structure['Default'] = ($structure['Default'] === '1');
504
                break;
505
        }
506
        $structure['Type'] = $type;
507
        return $structure;
508
    }
509
510
    public static function modify(string $tableName, array $oldField, array $newField): string
511
    {
512
        $sql = 'ALTER TABLE ' . self::quoteTableName($tableName) . ' CHANGE ' . $oldField['Field'] . ' ' . $newField['Field'] . ' ';
513
        $sql .= $newField['Type'] . ' ';
514
        if (strtolower($newField['Null']) === 'no') {
515
            $sql .= 'NOT ';
516
        }
517
        $sql .= 'NULL';
518
        if ($newField['Default'] !== null) {
519
            if ($newField['Type'] === 'boolean') {
520
                $newField['Default'] = $newField['Default'] ? '1' : '0';
521
            }
522
            $sql .= ' DEFAULT "' . $newField['Default'] . '"';
523
        }
524
        $sql .= ';';
525
        return $sql;
526
    }
527
528
    /**
529
     * Obtains an array of indexes for a table
530
     *
531
     * @param string $tableName
532
     *
533
     * @return array
534
     * @throws \DebugBar\DebugBarException
535
     */
536
    public static function getIndexes(string $tableName): array
537
    {
538
        $query = self::getIndexesSql($tableName);
539
        $data = DB::select($query);
540
        $result = [];
541
        foreach ($data as $value) {
542
            $row = self::normalizeIndexes($value);
543
            $result[$row['index']] = $row;
544
        }
545
546
        return $result;
547
    }
548
549
    public static function createIndex(string $tableName, string $index, array $data): string
550
    {
551
        $name = $data['column'];
552
        $primary = $data['primary'] === 'yes';
553
        $unique = $data['unique'] === 'yes';
554
        $sql = "ALTER TABLE `$tableName` ADD CONSTRAINT `$index` ";
555
        if ($primary) {
556
            $sql .= "PRIMARY KEY(`$name`)";
557
        }
558
        if ($unique) {
559
            $sql .= "UNIQUE(`$name`)";
560
        }
561
        $sql .= ';';
562
563
        return $sql;
564
    }
565
566
    public static function changeIndex(string $tableName, string $index, array $oldData, array $newData): string
567
    {
568
        $oldPrimary = $oldData['index'] === 'PRIMARY';
569
        $oldUnique = $oldData['unique'] === 1;
570
571
        $newPrimary = $newData['primary'] === 'yes';
572
        $newUnique = $newData['unique'] === 'yes';
573
574
        $ok = true;
575
        $ok = $ok && ($oldData['column'] === $newData['column']);
576
        $ok = $ok && ($oldPrimary === $newPrimary);
577
578
        // Si es primaria, es unique siempre así que solo comprobamos si no es unique
579
        if ($ok && !$oldPrimary) {
580
            $ok = $ok && $oldUnique === $newUnique;
581
        }
582
583
        // No hay cambios
584
        if ($ok) {
585
            return '';
586
        }
587
588
        $name = $newData['column'];
589
590
        $sql = "ALTER TABLE `$tableName` ADD CONSTRAINT  `$index` ";
591
        if ($newPrimary) {
592
            $sql .= "PRIMARY KEY(`$name`)";
593
        }
594
        if ($newUnique) {
595
            $sql .= "UNIQUE(`$name`)";
596
        }
597
        $sql .= ';';
598
599
        return $sql;
600
    }
601
602
    public static function removeIndex(string $tableName, string $index): string
603
    {
604
        $sql = "ALTER TABLE `$tableName` DROP CONSTRAINT `$index`";
605
606
        return $sql;
607
    }
608
609
}
610