Passed
Push — dbal ( 52c0e6...62c715 )
by Greg
05:49
created

getConstraintDataFromInformationSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 9
rs 10
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\DB\Drivers;
21
22
use Fisharebest\Webtrees\DB\Exceptions\SchemaException;
23
use Fisharebest\Webtrees\DB\Expression;
24
use Fisharebest\Webtrees\DB\Schema\BinaryColumn;
25
use Fisharebest\Webtrees\DB\Schema\CharacterColumn;
26
use Fisharebest\Webtrees\DB\Schema\ColumnInterface;
27
use Fisharebest\Webtrees\DB\Schema\IntegerColumn;
28
use Fisharebest\Webtrees\DB\Schema\Schema;
29
use Fisharebest\Webtrees\DB\Schema\Table;
30
31
use function array_filter;
32
use function implode;
33
use function is_int;
34
use function is_string;
35
use function preg_match;
36
use function str_starts_with;
37
38
/**
39
 * Driver for MySQL
40
 */
41
class MySQLDriver extends AbstractDriver implements DriverInterface
42
{
43
    protected const IDENTIFIER_OPEN_QUOTE  = '`';
44
    protected const IDENTIFIER_CLOSE_QUOTE = '`';
45
46
    private const INTEGER_TYPES = [
47
        8  => 'TINYINT',
48
        16 => 'SMALLINT',
49
        24 => 'MEDIUMINT',
50
        32 => 'INT',
51
        64 => 'BIGINT',
52
    ];
53
54
    /**
55
     * @param string|null $schema_name
56
     *
57
     * @return Schema
58
     * @throws SchemaException
59
     */
60
    public function introspectSchema(string $schema_name = null): Schema
61
    {
62
        $schema_name     ??= $this->query(sql: 'SELECT DATABASE() AS schema_name')[0]->schema_name;
63
        $pattern         = $this->escapeLike(string: $this->prefix) . '%';
64
        $table_data      = $this->getTableDataFromInformationSchema(schema_name: $schema_name, pattern: $pattern);
65
        $column_data     = $this->getColumnDataFromInformationSchema(schema_name: $schema_name, pattern: $pattern);
66
        $constraint_data = $this->getConstraintDataFromInformationSchema(schema_name: $schema_name, pattern: $pattern);
67
        $index_data      = $this->getIndexDataFromInformationSchema(schema_name: $schema_name, pattern: $pattern);
68
69
        $schema = new Schema();
70
71
        foreach ($table_data as $table_datum) {
72
            $table = new Table($table_datum->TABLE_NAME);
73
74
            $this_column_rows = array_filter($column_data, static fn(object $row): bool => $row->TABLE_NAME === $table_datum->TABLE_NAME);
75
76
            foreach ($this_column_rows as $this_column_row) {
77
                $name      = $this_column_row->COLUMN_NAME;
78
                $type      = $this_column_row->COLUMN_TYPE;
79
                $collation = $this_column_row->COLLATION_NAME;
80
                $default   = $this_column_row->COLUMN_DEFAULT;
81
82
                $column = $this
83
                    ->columnDefinitionToColumn($name, $type, $collation)
84
                    ->nullable($this_column_row->IS_NULLABLE === 'YES')
85
                    ->default($default);
86
87
                $table->addColumn($column);
88
            }
89
90
            $schema->addTable($table);
91
92
            print_r($this->createTableSQL($table) . '<br>');
93
        }
94
95
        var_dump($table_data, $column_data, $constraint_data, $index_data);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($table_data, $c...aint_data, $index_data) looks like debug code. Are you sure you do not want to remove it?
Loading history...
96
97
        return $schema;
98
    }
99
100
    /**
101
     * @param Table $table
102
     *
103
     * @return string
104
     */
105
    public function createTableSQL(Table $table): string
106
    {
107
        $columns = array_map(fn(ColumnInterface $column): string => $this->columnSQL($column), $table->getColumns());
108
109
        return 'CREATE TABLE ' . $this->quoteIdentifier($table->name) . ' (' . implode(', ', $columns) . ')';
110
    }
111
112
    private function columnSQL(ColumnInterface $column): string
113
    {
114
        $sql = $this->quoteIdentifier($column->name) . ' ';
0 ignored issues
show
Bug introduced by
Accessing name on the interface Fisharebest\Webtrees\DB\Schema\ColumnInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
115
116
        if ($column instanceof IntegerColumn) {
117
            $sql .= self::INTEGER_TYPES[$column->bits];
118
119
            if ($column->auto_increment) {
120
                $sql .= ' AUTO_INCREMENT';
121
            }
122
        } elseif ($column instanceof CharacterColumn) {
123
            $sql .= $column->varying ? 'VARCHAR' : 'CHAR';
124
            $sql .= '(' . $column->length . ')';
125
        } elseif ($column instanceof BinaryColumn) {
126
            $sql .= $column->varying ? 'VARBINARY' : 'BINARY';
127
            $sql .= '(' . $column->length . ')';
128
        }
129
130
        if ($column->isNullable()) {
131
            $sql .= ' NULLABLE';
132
        }
133
134
        if ($column->getDefault() instanceof Expression || is_int($column->getDefault())) {
135
            $sql .= ' DEFAULT ' . $column->getDefault();
136
        } elseif (is_string($column->getDefault())) {
137
            $sql .= ' DEFAULT ' . $this->quoteValue($column->getDefault());
138
        }
139
140
        if ($column->isInvisible()) {
141
            $sql .= ' INVISIBLE';
142
        }
143
144
        if ($column->getComment() !== '') {
145
            $sql .= ' COMMENT ' . $this->quoteValue($column->getComment());
146
        }
147
148
        return $sql;
149
    }
150
151
    /**
152
     * Generate a Column object from a row from INFORMATION_SCHEMA.COLUMNS
153
     *
154
     * @param string      $name
155
     * @param string      $type
156
     * @param string|null $collation
157
     *
158
     * @return ColumnInterface
159
     */
160
    private function columnDefinitionToColumn(string $name, string $type, string|null $collation): ColumnInterface
161
    {
162
        if (preg_match('/^bit\((\d+)\)$/', $type, $match) === 1) {
163
            return Schema::bit(name: $name, bits: (int) $match[1]);
164
        }
165
166
        if (preg_match('/^decimal\((\d+),(\d+)\)$/', $type, $match) === 1) {
167
            return Schema::decimal(name: $name, precision: (int) $match[1], scale: (int) $match[2]);
168
        }
169
170
        if (preg_match('/^varchar\((\d+)\)$/', $type, $match) === 1) {
171
            if (str_starts_with($collation ?? '', 'ascii')) {
172
                return Schema::varchar(name: $name, length: (int) $match[1]);
173
            }
174
175
            return Schema::nVarchar(name: $name, length: (int) $match[1]);
176
        }
177
178
        if (preg_match('/^char\((\d+)\)$/', $type, $match) === 1) {
179
            if (str_starts_with($collation ?? '', 'ascii')) {
180
                return Schema::char(name: $name, length: (int) $match[1]);
181
            }
182
183
            return Schema::nChar(name: $name, length: (int) $match[1]);
184
        }
185
186
        return match ($type) {
187
            'bigint'             => Schema::bigInteger(name: $name),
188
            'bigint unsigned'    => Schema::bigInteger(name: $name)->unsigned(),
189
            'blob'               => Schema::blob(name: $name, length: 2),
190
            'date'               => Schema::date(name: $name),
191
            'datetime'           => Schema::datetime(name: $name),
192
            'datetime(0)'        => Schema::datetime(name: $name),
193
            'datetime(1)'        => Schema::datetime(name: $name, length: 1),
194
            'datetime(2)'        => Schema::datetime(name: $name, length: 2),
195
            'datetime(3)'        => Schema::datetime(name: $name, length: 3),
196
            'datetime(4)'        => Schema::datetime(name: $name, length: 4),
197
            'datetime(5)'        => Schema::datetime(name: $name, length: 5),
198
            'datetime(6)'        => Schema::datetime(name: $name, length: 6),
199
            'double'             => Schema::double(name: $name),
200
            'float'              => Schema::float(name: $name),
201
            'geometry'           => Schema::geometry(name: $name),
202
            'geometrycollection' => Schema::geometrycollection(name: $name),
203
            'int'                => Schema::integer(name: $name),
204
            'int unsigned'       => Schema::integer(name: $name)->unsigned(),
205
            'json'               => Schema::json(name: $name),
206
            'linestring'         => Schema::linestring(name: $name),
207
            'longblob'           => Schema::blob(name: $name),
208
            'longtext'           => Schema::text(name: $name),
209
            'mediumblob'         => Schema::blob(name: $name, length: 3),
210
            'mediumint'          => Schema::mediumInteger(name: $name),
211
            'mediumint unsigned' => Schema::mediumInteger(name: $name)->unsigned(),
212
            'mediumtext'         => Schema::text(name: $name, length: 3),
213
            'multilinestring'    => Schema::multilinestring(name: $name),
214
            'multipoint'         => Schema::multipoint(name: $name),
215
            'multipolygon'       => Schema::multipolygon(name: $name),
216
            'point'              => Schema::point(name: $name),
217
            'polygon'            => Schema::polygon(name: $name),
218
            'smallint'           => Schema::smallInteger(name: $name),
219
            'smallint unsigned'  => Schema::smallInteger(name: $name)->unsigned(),
220
            'text'               => Schema::text(name: $name, length: 2),
221
            'time'               => Schema::time(name: $name),
222
            'timestamp'          => Schema::timestamp(name: $name),
223
            'timestamp(0)'       => Schema::timestamp(name: $name),
224
            'timestamp(1)'       => Schema::timestamp(name: $name, length: 1),
225
            'timestamp(2)'       => Schema::timestamp(name: $name, length: 2),
226
            'timestamp(3)'       => Schema::timestamp(name: $name, length: 3),
227
            'timestamp(4)'       => Schema::timestamp(name: $name, length: 4),
228
            'timestamp(5)'       => Schema::timestamp(name: $name, length: 5),
229
            'timestamp(6)'       => Schema::timestamp(name: $name, length: 6),
230
            'tinyblob'           => Schema::blob(name: $name, length: 1),
231
            'tinyint'            => Schema::tinyInteger(name: $name),
232
            'tinyint unsigned'   => Schema::tinyInteger(name: $name)->unsigned(),
233
            'tinytext'           => Schema::text(name: $name, length: 1),
234
            'year'               => Schema::year(name: $name),
235
            default              => throw new SchemaException('Unknown type ' . $type . ' for column ' . $name),
236
        };
237
    }
238
239
    /**
240
     * @param string $schema_name
241
     * @param string $pattern
242
     *
243
     * @return array<object{'TABLE_NAME': string, 'ENGINE': string, 'AUTO_INCREMENT': string, 'TABLE_COLLATION': string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{'TABLE_NAME...LE_COLLATION': string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
244
     */
245
    private function getTableDataFromInformationSchema(string $schema_name, string $pattern): array
246
    {
247
        $sql =
248
            'SELECT    TABLE_NAME, ENGINE, AUTO_INCREMENT, TABLE_COLLATION' .
249
            ' FROM     INFORMATION_SCHEMA.TABLES' .
250
            ' WHERE    TABLE_TYPE   =    :table_type' .
251
            '   AND    TABLE_SCHEMA =    :table_schema' .
252
            '   AND    TABLE_NAME   LIKE :pattern' .
253
            ' ORDER BY TABLE_NAME';
254
255
        return $this->query(sql: $sql, bindings: ['table_type' => 'BASE TABLE', 'table_schema' => $schema_name, 'pattern' => $pattern]);
256
    }
257
258
    /**
259
     * @param string $schema_name
260
     * @param string $pattern
261
     *
262
     * @return array<object{'TABLE_NAME':string, 'COLUMN_NAME': string, 'COLUMN_DEFAULT': string, 'IS_NULLABLE': string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{'TABLE_NAME...'IS_NULLABLE': string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
263
     */
264
    private function getColumnDataFromInformationSchema(string $schema_name, string $pattern): array
265
    {
266
        $sql =
267
            'SELECT    TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, DATETIME_PRECISION, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE, COLUMN_KEY, EXTRA, COLUMN_COMMENT, GENERATION_EXPRESSION, SRS_ID' .
268
            ' FROM     INFORMATION_SCHEMA.COLUMNS' .
269
            ' WHERE    TABLE_SCHEMA =    :table_schema' .
270
            '   AND    TABLE_NAME   LIKE :pattern' .
271
            ' ORDER BY ORDINAL_POSITION';
272
273
        return $this->query(sql: $sql, bindings: ['table_schema' => $schema_name, 'pattern' => $pattern]);
274
    }
275
276
    /**
277
     * @param string $schema_name
278
     * @param string $pattern
279
     *
280
     * @return array<object{'TABLE_NAME':string, 'CONSTRAINT_NAME': string, 'CONSTRAINT_TYPE': string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{'TABLE_NAME...STRAINT_TYPE': string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
281
     */
282
    private function getConstraintDataFromInformationSchema(string $schema_name, string $pattern): array
283
    {
284
        $sql =
285
            'SELECT TABLE_NAME, CONSTRAINT_NAME, CONSTRAINT_TYPE' .
286
            ' FROM  INFORMATION_SCHEMA.TABLE_CONSTRAINTS' .
287
            ' WHERE TABLE_SCHEMA =    :table_schema' .
288
            '   AND TABLE_NAME   LIKE :pattern';
289
290
        return $this->query(sql: $sql, bindings: ['table_schema' => $schema_name, 'pattern' => $pattern]);
291
    }
292
293
    /**
294
     * @param string $schema_name
295
     * @param string $pattern
296
     *
297
     * @return array<object{'CONSTRAINT_NAME':string, 'TABLE_NAME':string, 'COLUMN_NAME': string, 'ORDINAL_POSITION': string, 'POSITION_IN_UNIQUE_CONSTRAINT': string, 'REFERENCED_TABLE_NAME': string, 'REFERENCED_COLUMN_NAME': string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{'CONSTRAINT..._COLUMN_NAME': string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
298
     */
299
    private function getIndexDataFromInformationSchema(string $schema_name, string $pattern): array
300
    {
301
        $sql =
302
            'SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, POSITION_IN_UNIQUE_CONSTRAINT,' .
303
            '       REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME' .
304
            ' FROM  INFORMATION_SCHEMA.KEY_COLUMN_USAGE' .
305
            ' WHERE TABLE_SCHEMA = :table_schema' .
306
            '   AND TABLE_NAME LIKE :pattern';
307
308
        return $this->query(sql: $sql, bindings: ['table_schema' => $schema_name, 'pattern' => $pattern]);
309
    }
310
}
311